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}