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