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