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