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;
076import org.ametys.web.repository.site.SiteManager;
077
078/**
079 * Abstract class for all {@link Searchable} based on {@link Content}s
080 */
081public abstract class AbstractContentBasedSearchable extends AbstractParameterAdderSearchable implements Initializable, Contextualizable, Disposable, Cacheable
082{
083    // Push ids of System Properties you do not want to appear in the list
084    private static final List<String> __EXCLUDED_SYSTEM_PROPERTIES = Arrays.asList("site", 
085                                                                                    "parents", 
086                                                                                    "workflowStep");
087    // Push ids of Indexing Fields you do not want to appear in the list (for instance title which is handled separately)
088    private static final List<String> __EXCLUDED_INDEXING_FIELD = Arrays.asList("title");
089    
090    private static final String __SEARCH_CRITERION_CACHE_ID = AbstractContentBasedSearchable.class.getName() + "$SearchCriterionCache";
091    /** The id of extension point */
092    protected String _id;
093    /** The label */
094    protected I18nizableText _label;
095    /** The criteria position */
096    protected int _criteriaPosition;
097    /** The page returnable */
098    protected Returnable _pageReturnable;
099    /** The associated content returnable */
100    protected Returnable _associatedContentReturnable;
101    
102    /** The extension point for content types */
103    protected ContentTypeExtensionPoint _cTypeEP;
104    
105    /** The content helper */
106    protected ContentHelper _contentHelper;
107    
108    /** The search component helper */
109    protected SearchComponentHelper _searchComponentHelper;
110
111    private Context _context;
112    private ReturnableExtensionPoint _returnableEP;
113    private SystemPropertyExtensionPoint _systemPropertyEP;
114    private AmetysObjectResolver _ametysObjectResolver;
115    private TagProviderExtensionPoint _tagProviderEP;
116    private JSONUtils _jsonUtils;
117    private SiteManager _siteManager;
118    private AbstractCacheManager _abstractCacheManager;
119    
120    private List<ContentSearchCriterionDefinition> _systemPropertySearchCriterionDefs;
121    
122    private ContentSearchCriterionDefinition _titleIndexingFieldSearchCriterionDefCache;
123    private Set<ThreadSafeComponentManager<?>> _managers;
124    
125    @Override
126    public void configure(Configuration configuration) throws ConfigurationException
127    {
128        super.configure(configuration);
129        _id = configuration.getAttribute("id");
130        _label = I18nizableText.parseI18nizableText(configuration.getChild("label"), "plugin." + _pluginName);
131        _criteriaPosition = configuration.getChild("criteriaPosition").getValueAsInteger();
132    }
133    
134    @Override
135    public void service(ServiceManager manager) throws ServiceException
136    {
137        super.service(manager);
138        _returnableEP = (ReturnableExtensionPoint) manager.lookup(ReturnableExtensionPoint.ROLE);
139        _pageReturnable = _returnableEP.getExtension(PageReturnable.ROLE);
140        _systemPropertyEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
141        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
142        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
143        _tagProviderEP = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE);
144        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
145        _siteManager = (SiteManager) manager.lookup(SiteManager.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, _siteManager);
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<? extends ModelItem> indexingModelFields = contentType.getModelItems();
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<CriterionDefinitionAndSourceContentType> _createIndexingFieldSearchCriterionDefs(Collection<? extends ModelItem> indexingModelFields, ContentType requestedContentType)
396    {
397        List<CriterionDefinitionAndSourceContentType> criteria = new ArrayList<>();
398        String requestedContentTypeId = requestedContentType.getId();
399        
400        try
401        {
402            ThreadSafeComponentManager<SearchUICriterion> searchCriterionManager = new ThreadSafeComponentManager<>();
403            _managers.add(searchCriterionManager);
404            searchCriterionManager.setLogger(getLogger());
405            searchCriterionManager.contextualize(_context);
406            searchCriterionManager.service(_manager);
407            List<Pair<String, ContentType>> searchCriteriaRoles = new ArrayList<>();
408            
409            for (ModelItem indexingField : indexingModelFields)
410            {
411                if (__EXCLUDED_INDEXING_FIELD.contains(indexingField.getName()))
412                {
413                    continue;
414                }
415                // Get only first-level field (ignore composites and repeaters)
416                if (indexingField instanceof ElementDefinition)
417                {
418                    ContentType fromContentType = Optional.ofNullable(indexingField.getModel())
419                                                          .filter(ContentType.class::isInstance)
420                                                          .map(ContentType.class::cast)
421                                                          .orElse(requestedContentType);
422                    
423                    final String path = indexingField.getName();
424                    final String role = path;
425                    Configuration criteriaConf = _getIndexingFieldCriteriaConfiguration(path, Optional.of(requestedContentTypeId));
426                    searchCriterionManager.addComponent(_pluginName, null, role, IndexingFieldSearchUICriterion.class, criteriaConf);
427                    
428                    searchCriteriaRoles.add(Pair.of(role, fromContentType));
429                }
430            }
431            
432            searchCriterionManager.initialize();
433            
434            final String prefix = getIndexingFieldCriterionDefinitionPrefix();
435            for (Pair<String, ContentType> roleAndCType : searchCriteriaRoles)
436            {
437                String searchCriteriaRole = roleAndCType.getLeft();
438                ContentType fromContentType = roleAndCType.getRight();
439                SearchUICriterion criterion = searchCriterionManager.lookup(searchCriteriaRole);
440                String id = prefix + fromContentType.getId() + "$" + searchCriteriaRole;
441                Optional<Validator> validator = _getValidator(fromContentType, searchCriteriaRole);
442                
443                ContentSearchCriterionDefinition criterionDef = _criterionDefinition(id, criterion, fromContentType, validator);
444                criteria.add(new CriterionDefinitionAndSourceContentType(criterionDef, fromContentType));
445            }
446        }
447        catch (Exception e)
448        {
449            throw new RuntimeException("An error occured when retrieving IndexingFieldSearchCriterionDefinitions", e);
450        }
451        
452        criteria.sort(Comparator.comparing(
453            critDefAndSourceCtype -> critDefAndSourceCtype._contentTypeId, 
454            new ContentTypeComparator(requestedContentType, _cTypeEP)
455                .reversed()));
456        return criteria;
457    }
458    
459    private Optional<Validator> _getValidator(ContentType contentType, String itemPath) 
460    {
461        if (contentType.hasModelItem(itemPath))
462        {
463            return Optional.of(contentType.getModelItem(itemPath))
464                    .filter(ElementDefinition.class::isInstance)
465                    .map(ElementDefinition.class::cast)
466                    .filter(ElementDefinition::isEditable)
467                    .map(ElementDefinition::getValidator);
468        }
469        
470        return Optional.empty();
471    }
472    
473    private ContentSearchCriterionDefinition _criterionDefinition(String id, SearchUICriterion criterion, ContentType fromContentType, Optional<Validator> validator)
474    {
475        boolean isAttributeContent = criterion.getType() == MetadataType.CONTENT;
476        return isAttributeContent 
477                ? new ContentAttributeContentSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.of(fromContentType), validator, _ametysObjectResolver, _cTypeEP, _contentHelper)
478                : new ContentSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.of(fromContentType), validator);
479    }
480    
481    @Override
482    public Query buildQuery(
483            AbstractTreeNode<FOSearchCriterion> criterionTree, 
484            Map<String, Object> userCriteria, 
485            Collection<Returnable> returnables,
486            Collection<Searchable> searchables, 
487            AdditionalParameterValueMap additionalParameters, 
488            String currentLang, 
489            Map<String, Object> contextualParameters)
490    {
491        return _searchComponentHelper.buildQuery(criterionTree, userCriteria, returnables, searchables, additionalParameters, currentLang, null, contextualParameters);
492    }
493    
494    private static class ContentTypeComparator implements Comparator<String>
495    {
496        /* The purpose here is to fill a Map with an Integer
497         * for each content type id in the hierarchy, and to base
498         * the comparator on those values.
499         * For instance, if we have the following hierarchy:
500         * 
501         * _______A______
502         * _____/___\____
503         * _____B____C___
504         * ____/_\___|___
505         * ___B1_B2__C1__
506         * 
507         * which means that <A extends B,C> & <B extends B1,B2> & <C extends C1>
508         * then we want to generate the Map:
509         * {A=1, B=2, B1=3, B2=4, C=5, C1=6}
510         * (which means we do a depth-first search with pre-order i.e. the children are processed after their parent, from left to right)
511         * (See also https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR))
512         * 
513         * Then with this map, we generate the following order:
514         * [A, B, B1, B2, C, C1]
515         * (which will then be reversed by #_createIndexingFieldSearchCriterionDefs)
516         */
517        String _baseCTypeId;
518        private Map<String, Integer> _orderByContentType;
519        
520        ContentTypeComparator(ContentType baseContentType, ContentTypeExtensionPoint cTypeEP)
521        {
522            _baseCTypeId = baseContentType.getId();
523            _orderByContentType = new HashMap<>();
524            Incrementor incrementor = Incrementor.create()
525                    .withStart(0)
526                    .withMaximalCount(Integer.MAX_VALUE);
527            _fillOrderByContentType(baseContentType, incrementor, cTypeEP);
528        }
529        
530        private void _fillOrderByContentType(ContentType contentType, Incrementor incrementor, ContentTypeExtensionPoint cTypeEP)
531        {
532            String contentTypeId = contentType.getId();
533            incrementor.increment();
534            _orderByContentType.put(contentTypeId, incrementor.getCount());
535            Arrays.asList(contentType.getSupertypeIds())
536                    .stream()
537                    .sequential()
538                    .filter(id -> !_orderByContentType.containsKey(id)) // do not re-process already encountered content types
539                    .map(cTypeEP::getExtension)
540                    .filter(Objects::nonNull)
541                    .forEachOrdered(childContentType -> _fillOrderByContentType(childContentType, incrementor, cTypeEP));
542        }
543        
544        @Override
545        public int compare(String c1ContentTypeId, String c2ContentTypeId)
546        {
547            if (c1ContentTypeId.equals(c2ContentTypeId))
548            {
549                return 0;
550            }
551            
552            if (!_orderByContentType.containsKey(c1ContentTypeId) || !_orderByContentType.containsKey(c2ContentTypeId))
553            {
554                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());
555                throw new IllegalStateException(message);
556            }
557            
558            return Integer.compare(_orderByContentType.get(c1ContentTypeId), _orderByContentType.get(c2ContentTypeId));
559        }
560    }
561    
562    
563    private Collection<ContentSearchCriterionDefinition> _finalIndexingFieldCriterionDefs(Collection<CriterionDefinitionAndSourceContentType> indexingFieldCriterionDefs)
564    {
565        return indexingFieldCriterionDefs
566                .stream()
567                // 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
568                // this is done by calling #distinct(), and thus this is based on the #equals impl of CriterionDefinitionAndSourceContentType
569                .distinct()
570                .map(CriterionDefinitionAndSourceContentType::criterionDefinition)
571                .collect(Collectors.toList());
572    }
573    
574    private synchronized ContentSearchCriterionDefinition _getTitleIndexingFieldSearchCriterionDef()
575    {
576        if (_titleIndexingFieldSearchCriterionDefCache != null)
577        {
578            // found in cache
579            getLogger().info("'title' IndexingFieldSearchUICriterion cache hit.");
580        }
581        else
582        {
583            // add in cache
584            _titleIndexingFieldSearchCriterionDefCache = _createTitleIndexingFieldSearchCriterionDef();
585            getLogger().info("'title' IndexingFieldSearchUICriterion cache miss. It will be created and added in cache.");
586        }
587        return _titleIndexingFieldSearchCriterionDefCache;
588    }
589    
590    private ContentSearchCriterionDefinition _createTitleIndexingFieldSearchCriterionDef()
591    {
592        try
593        {
594            ThreadSafeComponentManager<SearchUICriterion> searchCriterionManager = new ThreadSafeComponentManager<>();
595            _managers.add(searchCriterionManager);
596            searchCriterionManager.setLogger(getLogger());
597            searchCriterionManager.contextualize(_context);
598            searchCriterionManager.service(_manager);
599            
600            String searchCriteriaRole = "title";
601            Configuration criteriaConf = _getIndexingFieldCriteriaConfiguration(searchCriteriaRole, Optional.empty());
602            searchCriterionManager.addComponent(_pluginName, null, searchCriteriaRole, IndexingFieldSearchUICriterion.class, criteriaConf);
603            
604            searchCriterionManager.initialize();
605            
606            SearchUICriterion criterion = searchCriterionManager.lookup(searchCriteriaRole);
607            String id = getIndexingFieldCriterionDefinitionPrefix() + "_common$" + searchCriteriaRole;
608            return new ContentSearchCriterionDefinition(id, _pluginName, Optional.of(this), criterion, Optional.empty(), Optional.empty());
609        }
610        catch (Exception e)
611        {
612            throw new RuntimeException("An error occured when retrieving IndexingFieldSearchCriterionDefinitions", e);
613        }
614    }
615    
616    /**
617     * Gets the prefix for the ids of indexing field criterion definitions
618     * @return the prefix for the ids of indexing field criterion definitions
619     */
620    protected String getIndexingFieldCriterionDefinitionPrefix()
621    {
622        return getCriterionDefinitionPrefix() + "indexingField$";
623    }
624    
625    private Configuration _getIndexingFieldCriteriaConfiguration(String path, Optional<String> contentTypeId)
626    {
627        DefaultConfiguration criteriaConf = new DefaultConfiguration("criteria");
628        DefaultConfiguration metaConf = new DefaultConfiguration("field");
629        criteriaConf.addChild(metaConf);
630        metaConf.setAttribute("path", path);
631        
632        DefaultConfiguration cTypesConf = new DefaultConfiguration("contentTypes");
633        criteriaConf.addChild(cTypesConf);
634        if (contentTypeId.isPresent())
635        {
636            DefaultConfiguration baseTypeConf = new DefaultConfiguration("baseType");
637            baseTypeConf.setAttribute("id", contentTypeId.get());
638            cTypesConf.addChild(baseTypeConf);
639        }
640        
641        return criteriaConf;
642    }
643    
644    @Override
645    public Collection<Returnable> relationsWith()
646    {
647        return Arrays.asList(_pageReturnable, _associatedContentReturnable);
648    }
649    
650    // wraps a CriterionDefinition and where it comes from
651    private static class CriterionDefinitionAndSourceContentType
652    {
653        String _contentTypeId;
654        @ExcludeFromSizeCalculation
655        private ContentSearchCriterionDefinition _critDef;
656        private String _critDefId;
657        
658        CriterionDefinitionAndSourceContentType(ContentSearchCriterionDefinition critDef, ContentType contentType)
659        {
660            _critDef = critDef;
661            _critDefId = critDef.getId();
662            _contentTypeId = contentType.getId();
663        }
664        
665        ContentSearchCriterionDefinition criterionDefinition()
666        {
667            return _critDef;
668        }
669        
670        @Override
671        public String toString()
672        {
673            return _critDefId;
674        }
675
676        @Override
677        public int hashCode()
678        {
679            final int prime = 31;
680            int result = 1;
681            result = prime * result + ((_contentTypeId == null) ? 0 : _contentTypeId.hashCode());
682            result = prime * result + ((_critDefId == null) ? 0 : _critDefId.hashCode());
683            return result;
684        }
685
686        @Override
687        public boolean equals(Object obj)
688        {
689            if (this == obj)
690            {
691                return true;
692            }
693            if (obj == null)
694            {
695                return false;
696            }
697            if (!(obj instanceof CriterionDefinitionAndSourceContentType))
698            {
699                return false;
700            }
701            CriterionDefinitionAndSourceContentType other = (CriterionDefinitionAndSourceContentType) obj;
702            if (_contentTypeId == null)
703            {
704                if (other._contentTypeId != null)
705                {
706                    return false;
707                }
708            }
709            else if (!_contentTypeId.equals(other._contentTypeId))
710            {
711                return false;
712            }
713            if (_critDefId == null)
714            {
715                if (other._critDefId != null)
716                {
717                    return false;
718                }
719            }
720            else if (!_critDefId.equals(other._critDefId))
721            {
722                return false;
723            }
724            return true;
725        }
726    }
727}