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