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