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