001/*
002 *  Copyright 2024 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.cms.search.model;
017
018import java.util.Arrays;
019import java.util.Map;
020import java.util.Set;
021
022import org.apache.avalon.framework.component.Component;
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.avalon.framework.service.Serviceable;
026import org.apache.commons.lang3.StringUtils;
027
028import org.ametys.cms.contenttype.ContentType;
029import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
030import org.ametys.cms.contenttype.ContentTypesHelper;
031import org.ametys.cms.model.ContentElementDefinition;
032import org.ametys.cms.model.properties.Property;
033import org.ametys.cms.repository.Content;
034import org.ametys.cms.search.QueryBuilder;
035import org.ametys.cms.search.content.ContentSearchHelper;
036import org.ametys.cms.search.model.impl.ContentReferencingSearchModelCriterionDefinition;
037import org.ametys.cms.search.model.impl.ContentTypesAwareReferencingCriterionDefinition;
038import org.ametys.cms.search.model.impl.LanguageAwareReferencingCriterionDefinition;
039import org.ametys.cms.search.model.impl.ReferencingSearchModelCriterionDefinition;
040import org.ametys.cms.search.model.impl.StaticContentReferencingSearchModelCriterionDefinition;
041import org.ametys.cms.search.model.impl.StaticContentTypesAwareReferencingCriterionDefinition;
042import org.ametys.cms.search.model.impl.StaticLanguageAwareReferencingCriterionDefinition;
043import org.ametys.cms.search.model.impl.StaticReferencingSearchModelCriterionDefinition;
044import org.ametys.cms.search.query.ContentTypeQuery;
045import org.ametys.cms.search.query.MixinTypeQuery;
046import org.ametys.cms.search.query.Query;
047import org.ametys.cms.search.query.Query.Operator;
048import org.ametys.cms.search.systemprop.ContentTypeSystemProperty;
049import org.ametys.cms.search.systemprop.LanguageSystemProperty;
050import org.ametys.runtime.model.ElementDefinition;
051import org.ametys.runtime.model.Model;
052import org.ametys.runtime.model.ModelHelper;
053import org.ametys.runtime.model.ModelItem;
054import org.ametys.runtime.plugin.component.AbstractLogEnabled;
055
056/**
057 * Helper for  referencing {@link CriterionDefinition}
058 */
059public class SearchModelCriterionDefinitionHelper extends AbstractLogEnabled implements Component, Serviceable
060{
061    /** The component role. */
062    public static final String ROLE = SearchModelCriterionDefinitionHelper.class.getName();
063    
064    /** The content type extension point */
065    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
066
067    /** The system property extension point */
068    protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
069    
070    /** The content types helper */
071    protected ContentTypesHelper _contentTypesHelper;
072    
073    /** The content search helper */
074    protected ContentSearchHelper _contentSearchHelper;
075    
076    /** The search model helper */
077    protected SearchModelHelper _searchModelHelper;
078    
079    /** The criterion definition helper */
080    protected CriterionDefinitionHelper _criterionDefinitionHelper;
081    
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
085        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
086        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
087        _contentSearchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
088        _searchModelHelper = (SearchModelHelper) manager.lookup(SearchModelHelper.ROLE);
089        _criterionDefinitionHelper = (CriterionDefinitionHelper) manager.lookup(CriterionDefinitionHelper.ROLE);
090    }
091    
092    /**
093     * Retrieves a {@link SearchModelCriterionDefinition} referencing the element at the given path
094     * @param searchModel The {@link SearchModel} defining this criterion
095     * @param referencePath the path of the element to reference
096     * @param contentTypeIds the identifiers of the content types containing the reference
097     * @return the {@link SearchModelCriterionDefinition}. Can be <code>null</code> if the reference can not be used as a criterion
098     */
099    public SearchModelCriterionDefinition createReferencingCriterionDefinition(Model searchModel, String referencePath, Set<String> contentTypeIds)
100    {
101        ElementDefinition reference = _contentSearchHelper.getReferenceFromFieldPath(referencePath, contentTypeIds);
102        SearchModelCriterionDefinition criterion = createReferencingCriterionDefinition(reference, referencePath);
103
104        if (criterion != null)
105        {
106            criterion.setModel(searchModel);
107        }
108        
109        return criterion;
110    }
111    
112    /**
113     * Retrieves a {@link SearchModelCriterionDefinition} referencing the given definition
114     * @param reference the definition of the element to reference
115     * @param referencePath the path of the reference from the model containing the reference
116     * @return the {@link SearchModelCriterionDefinition}. Can be <code>null</code> if the reference can not be used as a criterion
117     */
118    public SearchModelCriterionDefinition createReferencingCriterionDefinition(ElementDefinition reference, String referencePath)
119    {
120        if (reference instanceof Property && !(reference instanceof CriterionDefinitionAwareElementDefinition))
121        {
122            getLogger().warn("Unable to create a criterion definition for reference at path '" + referencePath + "'. this item will be ignored");
123            return null;
124        }
125        else if (!referencePath.contains(ModelItem.ITEM_PATH_SEPARATOR)
126                && reference instanceof LanguageSystemProperty)
127        {
128            return new LanguageAwareReferencingCriterionDefinition(reference, referencePath);
129        }
130        else if (!referencePath.contains(ModelItem.ITEM_PATH_SEPARATOR)
131                && reference instanceof ContentTypeSystemProperty contentTypeSystemProperty
132                && contentTypeSystemProperty.includeContentTypes())
133        {
134            return new ContentTypesAwareReferencingCriterionDefinition(reference, referencePath);
135        }
136        else if (reference instanceof ContentElementDefinition)
137        {
138            return new ContentReferencingSearchModelCriterionDefinition(reference, referencePath);
139        }
140        else
141        {
142            return new ReferencingSearchModelCriterionDefinition<>(reference, referencePath);
143        }
144    }
145
146    /**
147     * Retrieves the {@link Class} to use instantiate a criterion for the given reference
148     * @param contentTypes the content types declaring the referenced item
149     * @param reference the reference
150     * @return the {@link Class} to use instantiate the criterion. Can be <code>null</code> if the reference can not be used as a criterion
151     */
152    public Class< ? extends SearchModelCriterionDefinition> getStaticCriterionDefinitionClass(Set<ContentType> contentTypes, String reference)
153    {
154        Class<? extends SearchModelCriterionDefinition> criterionDefinitionClass = null;
155        
156        if (_systemPropertyExtensionPoint.hasExtension(reference))
157        {
158            SystemProperty systemProperty = _systemPropertyExtensionPoint.getExtension(reference);
159            if (!(systemProperty instanceof CriterionDefinitionAwareElementDefinition))
160            {
161                getLogger().warn("Unable to create a criterion definition for system property '" + reference + "'. this item will be ignored");
162            }
163            else if (systemProperty instanceof LanguageSystemProperty)
164            {
165                criterionDefinitionClass = StaticLanguageAwareReferencingCriterionDefinition.class;
166            }
167            else if (systemProperty instanceof ContentTypeSystemProperty contentTypeSystemProperty
168                    && contentTypeSystemProperty.includeContentTypes())
169            {
170                criterionDefinitionClass = StaticContentTypesAwareReferencingCriterionDefinition.class;
171            }
172            else
173            {
174                criterionDefinitionClass = StaticReferencingSearchModelCriterionDefinition.class;
175            }
176        }
177        else if (!contentTypes.isEmpty())
178        {
179            String[] referencePathSegments = StringUtils.split(reference, ModelItem.ITEM_PATH_SEPARATOR);
180            String lastReferencePathSegment = referencePathSegments[referencePathSegments.length - 1];
181            if (_systemPropertyExtensionPoint.hasExtension(lastReferencePathSegment))
182            {
183                criterionDefinitionClass = StaticReferencingSearchModelCriterionDefinition.class;
184            }
185            else if (ModelHelper.hasModelItem(reference, contentTypes))
186            {
187                ModelItem modelItem = ModelHelper.getModelItem(reference, contentTypes);
188                if (modelItem instanceof Property && !(modelItem instanceof CriterionDefinitionAwareElementDefinition))
189                {
190                    getLogger().warn("Unable to create a criterion definition for model item '" + reference + "'. this item will be ignored");
191                }
192                else if (modelItem instanceof ContentElementDefinition)
193                {
194                    criterionDefinitionClass = StaticContentReferencingSearchModelCriterionDefinition.class;
195                }
196                else
197                {
198                    criterionDefinitionClass = StaticReferencingSearchModelCriterionDefinition.class;
199                }
200            }
201        }
202        else if (Content.ATTRIBUTE_TITLE.equals(reference))
203        {
204            criterionDefinitionClass = StaticReferencingSearchModelCriterionDefinition.class;
205        }
206        
207        return criterionDefinitionClass;
208    }
209    
210    /**
211     * Retrieves the language associated to the given value.
212     * @param criterion the criterion
213     * @param value The user-submitted value (or the default value if not set) for the language criterion.
214     * @param allValues All the user-submitted values.
215     * @param contextualParameters the search contextual parameters.
216     * @return the language associated to the given value.
217     */
218    public String getLanguageValue(SearchModelCriterionDefinition criterion, Object value, Map<String, Object> allValues, Map<String, Object> contextualParameters)
219    {
220        Object langValue = value;
221        
222        if (_criterionDefinitionHelper.isQueryValueEmpty(value))
223        {
224            langValue = criterion.getDefaultValue();
225        }
226        
227        if (_criterionDefinitionHelper.isQueryValueEmpty(langValue))
228        {
229            return null;
230        }
231        else if (langValue instanceof String strLangValue)
232        {
233            // Get language from the contextual parameters to use it if no language has been defined in the criterion
234            String currentLanguage = (String) contextualParameters.get("language");
235            return "CURRENT".equals(strLangValue) ? currentLanguage : strLangValue;
236        }
237        else
238        {
239            throw new IllegalArgumentException("Try to get the language from non string value");
240        }
241    }
242    
243    /**
244     * Get the {@link Query} associated to the given value for the criterion referencing content types
245     * @param criterion the criterion
246     * @param reference the content types {@link SystemProperty} referenced by the given criterion
247     * @param value The user-submitted value (or the default value if not set) for the criterion.
248     * @param customOperator In advanced search mode, the operator chosen by the user. <code>null</code> to use the criterion-defined operator (simple search mode).
249     * @param allValues All the user-submitted values.
250     * @param language The current search language.
251     * @param contextualParameters the search contextual parameters.
252     * @return The content types {@link Query} associated to the given value.
253     */
254    public Query getContentTypesQuery(SearchModelCriterionDefinition criterion, ContentTypeSystemProperty reference, Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
255    {
256        Operator operator = customOperator != null ? customOperator : criterion.getOperator();
257        
258        if (_criterionDefinitionHelper.isQueryValueEmpty(value))
259        {
260            SearchModel model = (SearchModel) contextualParameters.get(QueryBuilder.SEARCH_MODEL);
261            Set<String> modelContentTypes = model.getContentTypes(contextualParameters);
262            return !modelContentTypes.isEmpty()
263                    ? _searchModelHelper.createContentTypeOrMixinQuery(modelContentTypes, operator)
264                    : null;
265        }
266        else if (reference.includeMixins())
267        {
268            if (criterion.isMultiple())
269            {
270                return _searchModelHelper.createContentTypeOrMixinQuery(Arrays.asList((String[]) value), operator);
271            }
272            else
273            {
274                String strValue = (String) value;
275                return _contentTypeExtensionPoint.getExtension(strValue).isMixin()
276                        ? new MixinTypeQuery(operator, strValue)
277                        : new ContentTypeQuery(operator, strValue);
278            }
279        }
280        else
281        {
282            return criterion.isMultiple()
283                    ? new ContentTypeQuery(operator, (String[]) value)
284                    : new ContentTypeQuery(operator, (String) value);
285        }
286    }
287}