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}