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