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