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