001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.frontoffice.search.metamodel.impl; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Comparator; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.Set; 029import java.util.stream.Collectors; 030 031import org.apache.avalon.framework.activity.Disposable; 032import org.apache.avalon.framework.activity.Initializable; 033import org.apache.avalon.framework.configuration.Configuration; 034import org.apache.avalon.framework.configuration.ConfigurationException; 035import org.apache.avalon.framework.configuration.DefaultConfiguration; 036import org.apache.avalon.framework.context.Context; 037import org.apache.avalon.framework.context.ContextException; 038import org.apache.avalon.framework.context.Contextualizable; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.commons.lang.StringUtils; 042import org.apache.commons.lang3.tuple.Pair; 043import org.apache.commons.math3.util.IntegerSequence.Incrementor; 044 045import org.ametys.cms.content.ContentHelper; 046import org.ametys.cms.contenttype.ContentType; 047import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 048import org.ametys.cms.contenttype.MetadataType; 049import org.ametys.cms.contenttype.indexing.IndexingField; 050import org.ametys.cms.contenttype.indexing.MetadataIndexingField; 051import org.ametys.cms.repository.Content; 052import org.ametys.cms.search.advanced.AbstractTreeNode; 053import org.ametys.cms.search.model.SystemProperty; 054import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 055import org.ametys.cms.search.query.Query; 056import org.ametys.cms.search.ui.model.SearchUICriterion; 057import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion; 058import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion; 059import org.ametys.cms.tag.TagProviderExtensionPoint; 060import org.ametys.core.cache.AbstractCacheManager; 061import org.ametys.core.cache.Cache; 062import org.ametys.core.util.Cacheable; 063import org.ametys.core.util.JSONUtils; 064import org.ametys.core.util.SizeUtils.ExcludeFromSizeCalculation; 065import org.ametys.plugins.repository.AmetysObjectResolver; 066import org.ametys.runtime.i18n.I18nizableText; 067import org.ametys.runtime.i18n.I18nizableTextParameter; 068import org.ametys.runtime.model.ElementDefinition; 069import org.ametys.runtime.model.ModelItem; 070import org.ametys.runtime.parameter.Validator; 071import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 072import org.ametys.web.frontoffice.search.instance.model.FOSearchCriterion; 073import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap; 074import org.ametys.web.frontoffice.search.metamodel.Returnable; 075import org.ametys.web.frontoffice.search.metamodel.ReturnableExtensionPoint; 076import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition; 077import org.ametys.web.frontoffice.search.metamodel.Searchable; 078import org.ametys.web.frontoffice.search.requesttime.impl.SearchComponentHelper; 079 080/** 081 * Abstract class for all {@link Searchable} based on {@link Content}s 082 */ 083public abstract class AbstractContentBasedSearchable extends AbstractParameterAdderSearchable implements Initializable, Contextualizable, Disposable, Cacheable 084{ 085 // Push ids of System Properties you do not want to appear in the list 086 private static final List<String> __EXCLUDED_SYSTEM_PROPERTIES = Arrays.asList("site", 087 "parents", 088 "workflowStep"); 089 // Push ids of Indexing Fields you do not want to appear in the list (for instance title which is handled separately) 090 private static final List<String> __EXCLUDED_INDEXING_FIELD = Arrays.asList("title"); 091 092 private static final String __SEARCH_CRITERION_CACHE_ID = AbstractContentBasedSearchable.class.getName() + "$SearchCriterionCache"; 093 /** The id of extension point */ 094 protected String _id; 095 /** The label */ 096 protected I18nizableText _label; 097 /** The criteria position */ 098 protected int _criteriaPosition; 099 /** The page returnable */ 100 protected Returnable _pageReturnable; 101 /** The associated content returnable */ 102 protected Returnable _associatedContentReturnable; 103 104 /** The extension point for content types */ 105 protected ContentTypeExtensionPoint _cTypeEP; 106 107 /** The content helper */ 108 protected ContentHelper _contentHelper; 109 110 /** The search component helper */ 111 protected SearchComponentHelper _searchComponentHelper; 112 113 private Context _context; 114 private ReturnableExtensionPoint _returnableEP; 115 private SystemPropertyExtensionPoint _systemPropertyEP; 116 private AmetysObjectResolver _ametysObjectResolver; 117 private TagProviderExtensionPoint _tagProviderEP; 118 private JSONUtils _jsonUtils; 119 private AbstractCacheManager _abstractCacheManager; 120 121 private List<ContentSearchCriterionDefinition> _systemPropertySearchCriterionDefs; 122 123 private ContentSearchCriterionDefinition _titleIndexingFieldSearchCriterionDefCache; 124 private Set<ThreadSafeComponentManager<?>> _managers; 125 126 @Override 127 public void configure(Configuration configuration) throws ConfigurationException 128 { 129 super.configure(configuration); 130 _id = configuration.getAttribute("id"); 131 _label = I18nizableText.parseI18nizableText(configuration.getChild("label"), "plugin." + _pluginName); 132 _criteriaPosition = configuration.getChild("criteriaPosition").getValueAsInteger(); 133 } 134 135 @Override 136 public void service(ServiceManager manager) throws ServiceException 137 { 138 super.service(manager); 139 _returnableEP = (ReturnableExtensionPoint) manager.lookup(ReturnableExtensionPoint.ROLE); 140 _pageReturnable = _returnableEP.getExtension(PageReturnable.ROLE); 141 _systemPropertyEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 142 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 143 _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 144 _tagProviderEP = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE); 145 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 146 _abstractCacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 147 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 148 _searchComponentHelper = (SearchComponentHelper) manager.lookup(SearchComponentHelper.ROLE); 149 } 150 151 /** 152 * Sets {@link #_associatedContentReturnable}. Called during {@link #initialize()} 153 */ 154 protected void _setAssociatedContentReturnable() 155 { 156 _associatedContentReturnable = _returnableEP.getExtension(associatedContentReturnableRole()); 157 } 158 159 /** 160 * The Avalon Role for the associated Content Returnable 161 * @return The Avalon Role for the associated Content Returnable 162 */ 163 protected abstract String associatedContentReturnableRole(); 164 165 @Override 166 public void contextualize(Context context) throws ContextException 167 { 168 _context = context; 169 } 170 171 @Override 172 public void initialize() throws Exception 173 { 174 _setAssociatedContentReturnable(); 175 _managers = new HashSet<>(); 176 177 _systemPropertySearchCriterionDefs = new ArrayList<>(); 178 for (String propId : _systemPropertyEP.getSearchProperties()) 179 { 180 if (__EXCLUDED_SYSTEM_PROPERTIES.contains(propId)) 181 { 182 continue; 183 } 184 185 ContentSearchCriterionDefinition def = _getSystemPropSearchCriterionDef(propId); 186 _systemPropertySearchCriterionDefs.add(def); 187 } 188 189 createCaches(); 190 } 191 192 public AbstractCacheManager getCacheManager() 193 { 194 return _abstractCacheManager; 195 } 196 197 @Override 198 public Collection<SingleCacheConfiguration> getManagedCaches() 199 { 200 return Arrays.asList( 201 SingleCacheConfiguration.of( 202 __SEARCH_CRITERION_CACHE_ID + _id, 203 _buildI18n("PLUGINS_WEB_SEARCH_CRITERION_CACHE_LABEL"), 204 _buildI18n("PLUGINS_WEB_SEARCH_CRITERION_CACHE_DESCRIPTION")) 205 ); 206 } 207 208 @Override 209 public boolean hasComputableSize() 210 { 211 return true; 212 } 213 214 private I18nizableText _buildI18n(String i18Key) 215 { 216 String catalogue = "plugin.web"; 217 Map<String, I18nizableTextParameter> params = Map.of("id", _label); 218 return new I18nizableText(catalogue, i18Key, params); 219 } 220 221 private Cache<String, Collection<CriterionDefinitionAndSourceContentType>> getSearchCriterionCache() 222 { 223 return getCacheManager().get(__SEARCH_CRITERION_CACHE_ID + _id); 224 } 225 226 227 private ContentSearchCriterionDefinition _getSystemPropSearchCriterionDef(String propId) 228 { 229 try 230 { 231 ThreadSafeComponentManager<SearchUICriterion> searchCriterionManager = new ThreadSafeComponentManager<>(); 232 _managers.add(searchCriterionManager); 233 searchCriterionManager.setLogger(getLogger()); 234 searchCriterionManager.contextualize(_context); 235 searchCriterionManager.service(_manager); 236 237 String role = propId; 238 Configuration criteriaConf = _getSystemCriteriaConfiguration(propId); 239 searchCriterionManager.addComponent(_pluginName, null, role, SystemSearchUICriterion.class, criteriaConf); 240 241 searchCriterionManager.initialize(); 242 243 SearchUICriterion criterion = searchCriterionManager.lookup(role); 244 String id = getSystemPropertyCriterionDefinitionPrefix() + role; 245 ContentSearchCriterionDefinition criterionDef = _criterionDefinition(id, criterion); 246 return criterionDef; 247 } 248 catch (Exception e) 249 { 250 throw new RuntimeException("An error occured when retrieving SystemPropertySearchCriterionDefinitions", e); 251 } 252 } 253 254 private ContentSearchCriterionDefinition _criterionDefinition(String id, SearchUICriterion criterion) 255 { 256 if (criterion instanceof SystemSearchUICriterion) 257 { 258 SystemSearchUICriterion systemCriterion = (SystemSearchUICriterion) criterion; 259 if ("tags".equals(systemCriterion.getSystemPropertyId())) 260 { 261 return new TagSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.empty(), _tagProviderEP, _jsonUtils); 262 } 263 } 264 return new ContentSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.empty(), Optional.empty()); 265 } 266 267 /** 268 * Gets the prefix for the ids of system property criterion definitions 269 * @return the prefix for the ids of system property criterion definitions 270 */ 271 protected String getSystemPropertyCriterionDefinitionPrefix() 272 { 273 return getCriterionDefinitionPrefix() + "systemProperty$"; 274 } 275 276 /** 277 * Gets the prefix for criterion definitions 278 * @return the prefix for criterion definitions 279 */ 280 protected abstract String getCriterionDefinitionPrefix(); 281 282 private Configuration _getSystemCriteriaConfiguration(String propertyId) 283 { 284 DefaultConfiguration conf = new DefaultConfiguration("criteria"); 285 286 DefaultConfiguration propConf = new DefaultConfiguration("systemProperty"); 287 propConf.setAttribute("name", propertyId); 288 conf.addChild(propConf); 289 290 // By default, SystemSearchUICriterion sets the multiple status to 'false', so here explicitly copy the multiple status from the property itself 291 SystemProperty property = _systemPropertyEP.getExtension(propertyId); 292 conf.setAttribute("multiple", property.isMultiple()); 293 294 return conf; 295 } 296 297 @Override 298 public void dispose() 299 { 300 _systemPropertySearchCriterionDefs 301 .forEach(ContentSearchCriterionDefinition::dispose); 302 _systemPropertySearchCriterionDefs.clear(); 303 304 getSearchCriterionCache().asMap().values() 305 .stream() 306 .flatMap(Collection::stream) 307 .map(CriterionDefinitionAndSourceContentType::criterionDefinition) 308 .forEach(ContentSearchCriterionDefinition::dispose); 309 getSearchCriterionCache().resetCache(); 310 removeCaches(); 311 312 _managers.forEach(ThreadSafeComponentManager::dispose); 313 _managers.clear(); 314 } 315 316 @Override 317 public I18nizableText getLabel() 318 { 319 return _label; 320 } 321 322 @Override 323 public int criteriaPosition() 324 { 325 return _criteriaPosition; 326 } 327 328 @Override 329 public Collection<SearchCriterionDefinition> getCriteria(AdditionalParameterValueMap additionalParameterValues) 330 { 331 Collection<SearchCriterionDefinition> criteria = new ArrayList<>(); 332 333 // Content types 334 Collection<String> contentTypes = getContentTypes(additionalParameterValues); 335 336 // Special case for title 337 criteria.add(_getTitleIndexingFieldSearchCriterionDef()); 338 339 // Indexing fields 340 Collection<CriterionDefinitionAndSourceContentType> indexingFieldCriterionDefs = _getIndexingFieldSearchCriterionDefs(contentTypes); 341 criteria.addAll(_finalIndexingFieldCriterionDefs(indexingFieldCriterionDefs)); 342 343 // System properties 344 criteria.addAll(_systemPropertySearchCriterionDefs); 345 346 if (getLogger().isInfoEnabled()) 347 { 348 getLogger().info("#getCriteria for contentTypes '{}' returned '{}'", 349 contentTypes, 350 criteria.stream() 351 .map(SearchCriterionDefinition::getId) 352 .collect(Collectors.toList())); 353 } 354 355 return criteria; 356 } 357 358 /** 359 * Gets the content types which will be used to retrieve the criteria relative to the Indexing Model 360 * @param additionalParameterValues The additional parameter values 361 * @return the content types which will be used to retrieve the criteria relative to the Indexing Model 362 */ 363 protected abstract Collection<String> getContentTypes(AdditionalParameterValueMap additionalParameterValues); 364 365 private synchronized Collection<CriterionDefinitionAndSourceContentType> _getIndexingFieldSearchCriterionDefs(Collection<String> contentTypeIds) 366 { 367 return contentTypeIds 368 .stream() 369 .map(this::_getIndexingFieldSearchCriterionDefs) 370 .flatMap(Collection::stream) 371 .collect(Collectors.toList()); 372 } 373 374 private Collection<CriterionDefinitionAndSourceContentType> _getIndexingFieldSearchCriterionDefs(String contentTypeId) 375 { 376 if (getSearchCriterionCache().hasKey(contentTypeId)) 377 { 378 // found in cache 379 Collection<CriterionDefinitionAndSourceContentType> defs = getSearchCriterionCache().get(contentTypeId); 380 getLogger().info("IndexingFieldSearchUICriteria for '{}' cache hit ({}).", contentTypeId, defs); 381 return defs; 382 } 383 else 384 { 385 ContentType contentType = _cTypeEP.getExtension(contentTypeId); 386 Collection<IndexingField> indexingModelFields = _getIndexingFields(contentType); 387 Collection<CriterionDefinitionAndSourceContentType> indexingFieldSearchCriterionDefs = _createIndexingFieldSearchCriterionDefs(indexingModelFields, contentType); 388 // add in cache 389 getSearchCriterionCache().put(contentTypeId, indexingFieldSearchCriterionDefs); 390 getLogger().info("IndexingFieldSearchUICriteria for '{}' cache miss. They will be created and added in cache ({}).", contentTypeId, indexingFieldSearchCriterionDefs); 391 return indexingFieldSearchCriterionDefs; 392 } 393 } 394 395 private Collection<IndexingField> _getIndexingFields(ContentType contentType) 396 { 397 return contentType 398 .getIndexingModel() 399 .getFields(); 400 } 401 402 private Collection<CriterionDefinitionAndSourceContentType> _createIndexingFieldSearchCriterionDefs(Collection<IndexingField> indexingModelFields, ContentType requestedContentType) 403 { 404 List<CriterionDefinitionAndSourceContentType> criteria = new ArrayList<>(); 405 String requestedContentTypeId = requestedContentType.getId(); 406 407 try 408 { 409 ThreadSafeComponentManager<SearchUICriterion> searchCriterionManager = new ThreadSafeComponentManager<>(); 410 _managers.add(searchCriterionManager); 411 searchCriterionManager.setLogger(getLogger()); 412 searchCriterionManager.contextualize(_context); 413 searchCriterionManager.service(_manager); 414 List<Pair<String, ContentType>> searchCriteriaRoles = new ArrayList<>(); 415 416 for (IndexingField indexingField : indexingModelFields) 417 { 418 if (__EXCLUDED_INDEXING_FIELD.contains(indexingField.getName())) 419 { 420 continue; 421 } 422 // Get only first-level field (ignore composites and repeaters) 423 if (indexingField.getType() != MetadataType.COMPOSITE) 424 { 425 ContentType fromContentType = Optional.of(indexingField) 426 .filter(MetadataIndexingField.class::isInstance) 427 .map(MetadataIndexingField.class::cast) 428 .map(indField -> _getReferenceContentType(indField, requestedContentType)) 429 .orElse(requestedContentType); 430 431 final String path = indexingField.getName(); 432 final String role = path; 433 Configuration criteriaConf = _getIndexingFieldCriteriaConfiguration(path, Optional.of(requestedContentTypeId)); 434 searchCriterionManager.addComponent(_pluginName, null, role, IndexingFieldSearchUICriterion.class, criteriaConf); 435 436 searchCriteriaRoles.add(Pair.of(role, fromContentType)); 437 } 438 } 439 440 searchCriterionManager.initialize(); 441 442 final String prefix = getIndexingFieldCriterionDefinitionPrefix(); 443 for (Pair<String, ContentType> roleAndCType : searchCriteriaRoles) 444 { 445 String searchCriteriaRole = roleAndCType.getLeft(); 446 ContentType fromContentType = roleAndCType.getRight(); 447 SearchUICriterion criterion = searchCriterionManager.lookup(searchCriteriaRole); 448 String id = prefix + fromContentType.getId() + "$" + searchCriteriaRole; 449 Optional<Validator> validator = _getValidator(fromContentType, searchCriteriaRole); 450 451 ContentSearchCriterionDefinition criterionDef = _criterionDefinition(id, criterion, fromContentType, validator); 452 criteria.add(new CriterionDefinitionAndSourceContentType(criterionDef, fromContentType)); 453 } 454 } 455 catch (Exception e) 456 { 457 throw new RuntimeException("An error occured when retrieving IndexingFieldSearchCriterionDefinitions", e); 458 } 459 460 criteria.sort(Comparator.comparing( 461 critDefAndSourceCtype -> critDefAndSourceCtype._contentTypeId, 462 new ContentTypeComparator(requestedContentType, _cTypeEP) 463 .reversed())); 464 return criteria; 465 } 466 467 private ContentType _getReferenceContentType(MetadataIndexingField indexingField, ContentType requestedContentType) 468 { 469 String metadataPath = indexingField.getMetadataPath(); 470 if (metadataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 471 { 472 String firstPath = StringUtils.substringBefore(metadataPath, ModelItem.ITEM_PATH_SEPARATOR); 473 return (ContentType) requestedContentType.getModelItem(firstPath).getModel(); 474 } 475 else 476 { 477 String contentId = indexingField.getMetadataDefinition().getReferenceContentType(); 478 return _cTypeEP.getExtension(contentId); 479 } 480 } 481 482 private Optional<Validator> _getValidator(ContentType contentType, String itemPath) 483 { 484 if (contentType.hasModelItem(itemPath)) 485 { 486 return Optional.of(contentType.getModelItem(itemPath)) 487 .filter(ElementDefinition.class::isInstance) 488 .map(ElementDefinition.class::cast) 489 .map(ElementDefinition::getValidator); 490 } 491 492 return Optional.empty(); 493 } 494 495 private ContentSearchCriterionDefinition _criterionDefinition(String id, SearchUICriterion criterion, ContentType fromContentType, Optional<Validator> validator) 496 { 497 boolean isAttributeContent = criterion.getType() == MetadataType.CONTENT; 498 return isAttributeContent 499 ? new ContentAttributeContentSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.of(fromContentType), validator, _ametysObjectResolver, _cTypeEP, _contentHelper) 500 : new ContentSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.of(fromContentType), validator); 501 } 502 503 @Override 504 public Query buildQuery( 505 AbstractTreeNode<FOSearchCriterion> criterionTree, 506 Map<String, Object> userCriteria, 507 Collection<Returnable> returnables, 508 Collection<Searchable> searchables, 509 AdditionalParameterValueMap additionalParameters, 510 String currentLang, 511 Map<String, Object> contextualParameters) 512 { 513 return _searchComponentHelper.buildQuery(criterionTree, userCriteria, returnables, searchables, additionalParameters, currentLang, null, contextualParameters); 514 } 515 516 private static class ContentTypeComparator implements Comparator<String> 517 { 518 /* The purpose here is to fill a Map with an Integer 519 * for each content type id in the hierarchy, and to base 520 * the comparator on those values. 521 * For instance, if we have the following hierarchy: 522 * 523 * _______A______ 524 * _____/___\____ 525 * _____B____C___ 526 * ____/_\___|___ 527 * ___B1_B2__C1__ 528 * 529 * which means that <A extends B,C> & <B extends B1,B2> & <C extends C1> 530 * then we want to generate the Map: 531 * {A=1, B=2, B1=3, B2=4, C=5, C1=6} 532 * (which means we do a depth-first search with pre-order i.e. the children are processed after their parent, from left to right) 533 * (See also https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR)) 534 * 535 * Then with this map, we generate the following order: 536 * [A, B, B1, B2, C, C1] 537 * (which will then be reversed by #_createIndexingFieldSearchCriterionDefs) 538 */ 539 String _baseCTypeId; 540 private Map<String, Integer> _orderByContentType; 541 542 ContentTypeComparator(ContentType baseContentType, ContentTypeExtensionPoint cTypeEP) 543 { 544 _baseCTypeId = baseContentType.getId(); 545 _orderByContentType = new HashMap<>(); 546 Incrementor incrementor = Incrementor.create() 547 .withStart(0) 548 .withMaximalCount(Integer.MAX_VALUE); 549 _fillOrderByContentType(baseContentType, incrementor, cTypeEP); 550 } 551 552 private void _fillOrderByContentType(ContentType contentType, Incrementor incrementor, ContentTypeExtensionPoint cTypeEP) 553 { 554 String contentTypeId = contentType.getId(); 555 incrementor.increment(); 556 _orderByContentType.put(contentTypeId, incrementor.getCount()); 557 Arrays.asList(contentType.getSupertypeIds()) 558 .stream() 559 .sequential() 560 .filter(id -> !_orderByContentType.containsKey(id)) // do not re-process already encountered content types 561 .map(cTypeEP::getExtension) 562 .filter(Objects::nonNull) 563 .forEachOrdered(childContentType -> _fillOrderByContentType(childContentType, incrementor, cTypeEP)); 564 } 565 566 @Override 567 public int compare(String c1ContentTypeId, String c2ContentTypeId) 568 { 569 if (c1ContentTypeId.equals(c2ContentTypeId)) 570 { 571 return 0; 572 } 573 574 if (!_orderByContentType.containsKey(c1ContentTypeId) || !_orderByContentType.containsKey(c2ContentTypeId)) 575 { 576 String message = String.format("An unexpected error occured with the ContentType comparator for base '%s', cannot compare '%s' and '%s'.\nThe orderByContentType map is: %s", _baseCTypeId, c1ContentTypeId, c2ContentTypeId, _orderByContentType.toString()); 577 throw new IllegalStateException(message); 578 } 579 580 return Integer.compare(_orderByContentType.get(c1ContentTypeId), _orderByContentType.get(c2ContentTypeId)); 581 } 582 } 583 584 585 private Collection<ContentSearchCriterionDefinition> _finalIndexingFieldCriterionDefs(Collection<CriterionDefinitionAndSourceContentType> indexingFieldCriterionDefs) 586 { 587 return indexingFieldCriterionDefs 588 .stream() 589 // we want to not have duplicates, i.e. having same indexing field brought by two ContentTypes because it is declared in their common super ContentType 590 // this is done by calling #distinct(), and thus this is based on the #equals impl of CriterionDefinitionAndSourceContentType 591 .distinct() 592 .map(CriterionDefinitionAndSourceContentType::criterionDefinition) 593 .collect(Collectors.toList()); 594 } 595 596 private synchronized ContentSearchCriterionDefinition _getTitleIndexingFieldSearchCriterionDef() 597 { 598 if (_titleIndexingFieldSearchCriterionDefCache != null) 599 { 600 // found in cache 601 getLogger().info("'title' IndexingFieldSearchUICriterion cache hit."); 602 } 603 else 604 { 605 // add in cache 606 _titleIndexingFieldSearchCriterionDefCache = _createTitleIndexingFieldSearchCriterionDef(); 607 getLogger().info("'title' IndexingFieldSearchUICriterion cache miss. It will be created and added in cache."); 608 } 609 return _titleIndexingFieldSearchCriterionDefCache; 610 } 611 612 private ContentSearchCriterionDefinition _createTitleIndexingFieldSearchCriterionDef() 613 { 614 try 615 { 616 ThreadSafeComponentManager<SearchUICriterion> searchCriterionManager = new ThreadSafeComponentManager<>(); 617 _managers.add(searchCriterionManager); 618 searchCriterionManager.setLogger(getLogger()); 619 searchCriterionManager.contextualize(_context); 620 searchCriterionManager.service(_manager); 621 622 String searchCriteriaRole = "title"; 623 Configuration criteriaConf = _getIndexingFieldCriteriaConfiguration(searchCriteriaRole, Optional.empty()); 624 searchCriterionManager.addComponent(_pluginName, null, searchCriteriaRole, IndexingFieldSearchUICriterion.class, criteriaConf); 625 626 searchCriterionManager.initialize(); 627 628 SearchUICriterion criterion = searchCriterionManager.lookup(searchCriteriaRole); 629 String id = getIndexingFieldCriterionDefinitionPrefix() + "_common$" + searchCriteriaRole; 630 return new ContentSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.empty(), Optional.empty()); 631 } 632 catch (Exception e) 633 { 634 throw new RuntimeException("An error occured when retrieving IndexingFieldSearchCriterionDefinitions", e); 635 } 636 } 637 638 /** 639 * Gets the prefix for the ids of indexing field criterion definitions 640 * @return the prefix for the ids of indexing field criterion definitions 641 */ 642 protected String getIndexingFieldCriterionDefinitionPrefix() 643 { 644 return getCriterionDefinitionPrefix() + "indexingField$"; 645 } 646 647 private Configuration _getIndexingFieldCriteriaConfiguration(String path, Optional<String> contentTypeId) 648 { 649 DefaultConfiguration criteriaConf = new DefaultConfiguration("criteria"); 650 DefaultConfiguration metaConf = new DefaultConfiguration("field"); 651 criteriaConf.addChild(metaConf); 652 metaConf.setAttribute("path", path); 653 654 DefaultConfiguration cTypesConf = new DefaultConfiguration("contentTypes"); 655 criteriaConf.addChild(cTypesConf); 656 if (contentTypeId.isPresent()) 657 { 658 DefaultConfiguration baseTypeConf = new DefaultConfiguration("baseType"); 659 baseTypeConf.setAttribute("id", contentTypeId.get()); 660 cTypesConf.addChild(baseTypeConf); 661 } 662 663 return criteriaConf; 664 } 665 666 @Override 667 public Collection<Returnable> relationsWith() 668 { 669 return Arrays.asList(_pageReturnable, _associatedContentReturnable); 670 } 671 672 // wraps a CriterionDefinition and where it comes from 673 private static class CriterionDefinitionAndSourceContentType 674 { 675 String _contentTypeId; 676 @ExcludeFromSizeCalculation 677 private ContentSearchCriterionDefinition _critDef; 678 private String _critDefId; 679 680 CriterionDefinitionAndSourceContentType(ContentSearchCriterionDefinition critDef, ContentType contentType) 681 { 682 _critDef = critDef; 683 _critDefId = critDef.getId(); 684 _contentTypeId = contentType.getId(); 685 } 686 687 ContentSearchCriterionDefinition criterionDefinition() 688 { 689 return _critDef; 690 } 691 692 @Override 693 public String toString() 694 { 695 return _critDefId; 696 } 697 698 @Override 699 public int hashCode() 700 { 701 final int prime = 31; 702 int result = 1; 703 result = prime * result + ((_contentTypeId == null) ? 0 : _contentTypeId.hashCode()); 704 result = prime * result + ((_critDefId == null) ? 0 : _critDefId.hashCode()); 705 return result; 706 } 707 708 @Override 709 public boolean equals(Object obj) 710 { 711 if (this == obj) 712 { 713 return true; 714 } 715 if (obj == null) 716 { 717 return false; 718 } 719 if (!(obj instanceof CriterionDefinitionAndSourceContentType)) 720 { 721 return false; 722 } 723 CriterionDefinitionAndSourceContentType other = (CriterionDefinitionAndSourceContentType) obj; 724 if (_contentTypeId == null) 725 { 726 if (other._contentTypeId != null) 727 { 728 return false; 729 } 730 } 731 else if (!_contentTypeId.equals(other._contentTypeId)) 732 { 733 return false; 734 } 735 if (_critDefId == null) 736 { 737 if (other._critDefId != null) 738 { 739 return false; 740 } 741 } 742 else if (!_critDefId.equals(other._critDefId)) 743 { 744 return false; 745 } 746 return true; 747 } 748 } 749}