001/*
002 *  Copyright 2025 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.impl;
017
018import java.util.HashMap;
019import java.util.HashSet;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023import java.util.Set;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfiguration;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.commons.lang3.StringUtils;
031
032import org.ametys.cms.contenttype.ContentType;
033import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
034import org.ametys.cms.data.type.ModelItemTypeExtensionPoint;
035import org.ametys.cms.data.type.indexing.IndexableElementType;
036import org.ametys.cms.model.CMSDataContext;
037import org.ametys.cms.model.ContentElementDefinition;
038import org.ametys.cms.model.properties.Property;
039import org.ametys.cms.search.content.ContentSearchHelper;
040import org.ametys.cms.search.content.ContentSearchHelper.JoinedPaths;
041import org.ametys.cms.search.model.CriterionDefinition;
042import org.ametys.cms.search.model.CriterionDefinitionAwareElementDefinition;
043import org.ametys.cms.search.model.IndexationAwareElementDefinition;
044import org.ametys.cms.search.query.JoinQuery;
045import org.ametys.cms.search.query.NotQuery;
046import org.ametys.cms.search.query.Query;
047import org.ametys.cms.search.query.Query.LogicalOperator;
048import org.ametys.cms.search.query.Query.Operator;
049import org.ametys.runtime.i18n.I18nizableText;
050import org.ametys.runtime.model.ElementDefinition;
051import org.ametys.runtime.model.Enumerator;
052import org.ametys.runtime.model.ModelHelper;
053import org.ametys.runtime.model.ModelItem;
054import org.ametys.runtime.model.exception.UnknownTypeException;
055import org.ametys.runtime.model.type.DataContext;
056import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
057
058/**
059 * Default implementation for {@link CriterionDefinition} searching on a model item.
060 * @param <T> Type of the criterion value
061 */
062public class ReferencingCriterionDefinition<T> extends AbstractCriterionDefinition<T>
063{
064    /** None value to search for reference where the attribute does not exist */
065    public static final String NONE_VALUE = "__ametys_none";
066    /** The content type extension point */
067    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
068    
069    /** The extension point containing all available criterion types */
070    protected ModelItemTypeExtensionPoint _criterionTypeExtensionPoint;
071    
072    /** The content search helper */
073    protected ContentSearchHelper _contentSearchHelper;
074    
075    /** The referenced definition used for this criterion */
076    protected ElementDefinition _reference;
077    
078    /** The path of the field from the containing search model */
079    protected String _referencePath;
080    
081    /** The identifiers of the content types defining the reference */
082    protected Set<String> _contentTypeIds = new HashSet<>();
083    
084    /**
085     * Default constructor.
086     */
087    public ReferencingCriterionDefinition()
088    {
089        // Nothing to do
090    }
091    
092    /**
093     * Constructor used to create a BO criterion definition on a referenced item
094     * @param reference the item referenced by this criterion
095     * @param referencePath the path of the criterion's reference
096     */
097    public ReferencingCriterionDefinition(ElementDefinition reference, String referencePath)
098    {
099        _reference = reference;
100        _referencePath = referencePath;
101    }
102    
103    /**
104     * Get the path in the criterion's reference.
105     * @return the path in the criterion's reference.
106     */
107    public String getReferencePath()
108    {
109        return _referencePath;
110    }
111    
112    /**
113     * Set the path of the criterion's reference
114     * @param referencePath the reference path to set
115     */
116    protected void setReferencePath(String referencePath)
117    {
118        _referencePath = referencePath;
119    }
120    
121    /**
122     * Retrieves the referenced element definition
123     * @return the referenced element definition
124     */
125    public ElementDefinition<T> getReference()
126    {
127        return _reference;
128    }
129
130    /**
131     * Set the referenced element definition
132     * @param reference the referenced element definition to set
133     */
134    protected void setReference(ElementDefinition<T> reference)
135    {
136        _reference = reference;
137    }
138    
139    /**
140     * Retrieves the identifiers of the content types defining the reference
141     * @param contextualParameters the contextual parameters
142     * @return the identifiers of the content types defining the reference
143     */
144    public Set<String> getContentTypeIds(Map<String, Object> contextualParameters)
145    {
146        return _contentTypeIds;
147    }
148    
149    /**
150     * Set the identifiers of the content types defining the reference
151     * @param contentTypeIds the content type identifiers to set
152     */
153    public void setContentTypeIds(Set<String> contentTypeIds)
154    {
155        _contentTypeIds = contentTypeIds;
156    }
157    
158    @Override
159    public I18nizableText getLabel()
160    {
161        I18nizableText label = super.getLabel();
162        return label != null ? label : getReference().getLabel();
163    }
164    
165    @Override
166    public I18nizableText getDescription()
167    {
168        I18nizableText description = super.getDescription();
169        return description != null ? description : getReference().getDescription();
170    }
171    
172    @SuppressWarnings("unchecked")
173    @Override
174    public IndexableElementType<T> getType()
175    {
176        if (getReference() instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference)
177        {
178            return criterionDefinitionAwareReference.getCriterionType();
179        }
180        else
181        {
182            IndexableElementType<T> referenceType = (IndexableElementType<T>) getReference().getType();
183            String criterionTypeId = referenceType.getCriterionTypeId(CMSDataContext.newInstance()
184                                                                                    .withModelItem(this));
185            
186            if (_getCriterionTypeExtensionPoint().hasExtension(criterionTypeId))
187            {
188                return (IndexableElementType<T>) _getCriterionTypeExtensionPoint().getExtension(criterionTypeId);
189            }
190            else
191            {
192                throw new UnknownTypeException("Unable to retrieve type of the criterion definition referencing '" + getReferencePath() + "'. The type '" + criterionTypeId + "' is not available for criteria");
193            }
194        }
195    }
196    
197    @Override
198    protected String _getDefaultWidget()
199    {
200        ElementDefinition<T> reference = getReference();
201        if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference)
202        {
203            String defaultWidget = criterionDefinitionAwareReference.getDefaultCriterionWidget();
204            return "edition.textarea".equals(defaultWidget) ? null : defaultWidget;
205        }
206        else
207        {
208            String defaultWidget = _getCriterionDefinitionHelper().getCriterionDefinitionDefaultWidget(this);
209            if (StringUtils.isEmpty(defaultWidget) && reference.isEditable())
210            {
211                defaultWidget = reference.getWidget();
212            }
213            
214            return defaultWidget;
215        }
216    }
217    
218    @Override
219    protected Map<String, I18nizableText> _getDefaultWidgetParameters()
220    {
221        ElementDefinition<T> reference = getReference();
222        if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference)
223        {
224            try
225            {
226                Configuration configuration = _getEnumeratorAndWidgetParamConf();
227                return criterionDefinitionAwareReference.getDefaultCriterionWidgetParameters(configuration);
228            }
229            catch (ConfigurationException e)
230            {
231                // An error occurs while trying to configure the criterion widget parameters of the system property => use the classic widget parameters
232                _logger.error("Unable to use widget parameters for criterion of the system property '" + criterionDefinitionAwareReference.getName() + "'", e);
233                return _getCriterionDefinitionHelper().getCriterionDefinitionDefaultWidgetParameters(this);
234            }
235        }
236        else
237        {
238            Map<String, I18nizableText> defaultWidgetParameters = new HashMap<>();
239            if (reference.isEditable())
240            {
241                defaultWidgetParameters.putAll(reference.getWidgetParameters());
242            }
243
244            defaultWidgetParameters.putAll(_getCriterionDefinitionHelper().getCriterionDefinitionDefaultWidgetParameters(this));
245            
246            return defaultWidgetParameters;
247        }
248    }
249    
250    @Override
251    public Enumerator<T> getEnumerator()
252    {
253        return Optional.ofNullable(super.getEnumerator())
254                       .orElseGet(() -> _getDefaultEnumerator());
255    }
256    
257    /**
258     * Retrieves the default {@link Enumerator} for the current criterion definition
259     * @return the default {@link Enumerator} for the current criterion definition
260     */
261    @SuppressWarnings("unchecked")
262    protected Enumerator<T> _getDefaultEnumerator()
263    {
264        ElementDefinition<T> reference = getReference();
265        if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference)
266        {
267            ThreadSafeComponentManager<Enumerator> enumeratorManager = new ThreadSafeComponentManager<>();
268            try
269            {
270                enumeratorManager.setLogger(_logger);
271                enumeratorManager.contextualize(__context);
272                enumeratorManager.service(__serviceManager);
273
274                Configuration configuration = _getEnumeratorAndWidgetParamConf();
275                return criterionDefinitionAwareReference.getDefaultCriterionEnumerator(configuration, enumeratorManager);
276            }
277            catch (ConfigurationException e)
278            {
279                // An error occurs while trying to configure the criterion enumerator of the reference => use the classic enumerator if exists
280                _logger.error("Unable to use enumerator for criterion of the reference '" + criterionDefinitionAwareReference.getName() + "'", e);
281                return reference.getEnumerator();
282            }
283            finally
284            {
285                enumeratorManager.dispose();
286                enumeratorManager = null;
287            }
288        }
289        else
290        {
291            return reference.getEnumerator();
292        }
293    }
294    
295    private Configuration _getEnumeratorAndWidgetParamConf() throws ConfigurationException
296    {
297        Set<String> contentTypeIds = new HashSet<>();
298        for (String contentTypeId : getContentTypeIds(new HashMap<>()))
299        {
300            contentTypeIds.add(contentTypeId);
301            contentTypeIds.addAll(_getContentTypeExtensionPoint().getSubTypes(contentTypeId));
302        }
303        Set<ContentType> contentTypes = contentTypeIds.stream()
304                                                      .map(_getContentTypeExtensionPoint()::getExtension)
305                                                      .collect(Collectors.toSet());
306        
307        String referencePath = getReferencePath();
308        if (referencePath.contains(ModelItem.ITEM_PATH_SEPARATOR))
309        {
310            String referencePathPrefix = StringUtils.substringBeforeLast(referencePath, ModelItem.ITEM_PATH_SEPARATOR);
311            ContentElementDefinition contentElementDefinition = (ContentElementDefinition) ModelHelper.getModelItem(referencePathPrefix, contentTypes);
312            String contentTypeId = contentElementDefinition.getContentTypeId();
313            contentTypes = Optional.ofNullable(contentTypeId)
314                                   .filter(StringUtils::isNotEmpty)
315                                   .map(id -> _getContentTypeExtensionPoint().getExtension(contentTypeId))
316                                   .map(contentType -> Set.of(contentType))
317                                   .orElseGet(() -> Set.of());
318        }
319        
320        Configuration wrap = _getCriterionDefinitionHelper().wrapCriterionConfiguration(_getRootCriterionConfiguration(), contentTypes);
321        
322        return wrap;
323    }
324    
325    /**
326     * Retrieves the root criterion configuration
327     * Used to wrap it for enumerator and widget parameters
328     * @return the root criterion configuration
329     */
330    protected Configuration _getRootCriterionConfiguration()
331    {
332        return new DefaultConfiguration("criterion");
333    }
334    
335    @SuppressWarnings("unchecked")
336    @Override
337    public Object convertQueryValue(Object value, Map<String, Object> contextualParameters)
338    {
339        return getReference() instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference
340            ? criterionDefinitionAwareReference.convertQueryValue(value, getReferencePath(), contextualParameters)
341            : super.convertQueryValue(value, contextualParameters);
342    }
343    
344    @SuppressWarnings("unchecked")
345    @Override
346    public Query getQuery(Object value, Operator operator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
347    {
348        if (operator != Operator.EXISTS && _getCriterionDefinitionHelper().isQueryValueEmpty(value))
349        {
350            return null;
351        }
352
353        ElementDefinition<T> reference = getReference();
354        Query query = null;
355        if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference)
356        {
357            // If value wanted is none, create query that search for contents with no value for the attribute
358            if (NONE_VALUE.equals(value))
359            {
360                query = new NotQuery(criterionDefinitionAwareReference.getQuery(value, Operator.EXISTS, language, contextualParameters));
361            }
362            else
363            {
364                query = criterionDefinitionAwareReference.getQuery(value, operator, language, contextualParameters);
365            }
366        }
367        else
368        {
369            boolean isMultipleOperandAnd = LogicalOperator.AND.equals(getMultipleOperandOperator());
370            CMSDataContext context = getQueryContext(language, contextualParameters);
371            
372            IndexableElementType referenceType = (IndexableElementType) reference.getType();
373            String queryFieldPath = _getContentSearchHelper().computeQueryFieldPath(reference);
374            
375            // If value wanted is none, create query that search for contents with no value for the attribute
376            if (NONE_VALUE.equals(value))
377            {
378                query = new NotQuery(referenceType.getDefaultQuery(value, queryFieldPath, Operator.EXISTS, isMultipleOperandAnd, context));
379            }
380            else
381            {
382                query = referenceType.getDefaultQuery(value, queryFieldPath, operator, isMultipleOperandAnd, context);
383            }
384        }
385        
386        List<String> queryJoinPaths = getJoinedPaths(contextualParameters);
387        if (query != null && !queryJoinPaths.isEmpty())
388        {
389            query = new JoinQuery(query, queryJoinPaths);
390        }
391        
392        return query;
393    }
394    
395    @Override
396    public String getSolrFacetFieldName(Map<String, Object> contextualParameters)
397    {
398        String solrFacetFieldName = super.getSolrFacetFieldName(contextualParameters);
399        
400        if (solrFacetFieldName == null && _isFacetable())
401        {
402            Set<String> contentTypeIds = getContentTypeIds(contextualParameters);
403            JoinedPaths joinedPaths =  _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds);
404            String fieldNameSuffix = _getFacetFieldNameSuffix();
405            solrFacetFieldName = joinedPaths.lastSegmentPrefix() + fieldNameSuffix;
406            setSolrFacetFieldName(solrFacetFieldName);
407        }
408
409        return solrFacetFieldName;
410    }
411    
412    /**
413     * Determine if the current criterion definition can be used as facet
414     * @return <code>true</code> if the current criterion definition can be used as facet, <code>false</code> otherwise
415     */
416    protected boolean _isFacetable()
417    {
418        ElementDefinition<T> reference = getReference();
419        return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition
420                ? indexationAwareElementDefinition.isFacetable()
421                : reference instanceof Property
422                    ? false
423                    : ((IndexableElementType) reference.getType()).isFacetable(DataContext.newInstance()
424                                                       .withModelItem(this));
425    }
426    
427    /**
428     * Retrieves the suffix of the facet field name
429     * @return the suffix of the facet field name
430     */
431    protected String _getFacetFieldNameSuffix()
432    {
433        ElementDefinition<T> reference = getReference();
434        return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition
435                ? indexationAwareElementDefinition.getSolrFacetFieldName()
436                : reference instanceof Property
437                    ? null
438                    : reference.getName() + ((IndexableElementType) reference.getType()).getFacetFieldSuffix(DataContext.newInstance()
439                                                                                                                        .withModelItem(reference));
440    }
441    
442    @Override
443    public List<String> getJoinedPaths(Map<String, Object> contextualParameters)
444    {
445        List<String> joinedPaths = super.getJoinedPaths(contextualParameters);
446        
447        if (joinedPaths.isEmpty())
448        {
449            Set<String> contentTypeIds = getContentTypeIds(contextualParameters);
450            JoinedPaths computedJoinedPaths = _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds);
451            joinedPaths = computedJoinedPaths.joinedPaths();
452            setJoinedPaths(joinedPaths);
453        }
454
455        return joinedPaths;
456    }
457    
458    /**
459     * Retrieves The content type extension point
460     * @return The content type extension point
461     */
462    protected ContentTypeExtensionPoint _getContentTypeExtensionPoint()
463    {
464        if (_contentTypeExtensionPoint == null)
465        {
466            try
467            {
468                _contentTypeExtensionPoint = (ContentTypeExtensionPoint) __serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
469            }
470            catch (ServiceException e)
471            {
472                throw new RuntimeException("Unable to lookup after the content type extension point", e);
473            }
474        }
475        
476        return _contentTypeExtensionPoint;
477    }
478    
479    /**
480     * Retrieves The extension point containing all available criterion types
481     * @return The extension point containing all available criterion types
482     */
483    protected ModelItemTypeExtensionPoint _getCriterionTypeExtensionPoint()
484    {
485        if (_criterionTypeExtensionPoint == null)
486        {
487            try
488            {
489                _criterionTypeExtensionPoint = (ModelItemTypeExtensionPoint) __serviceManager.lookup(ModelItemTypeExtensionPoint.ROLE_CRITERION_DEFINITION);
490            }
491            catch (ServiceException e)
492            {
493                throw new RuntimeException("Unable to lookup after the extension point containing all available criterion types", e);
494            }
495        }
496        
497        return _criterionTypeExtensionPoint;
498    }
499    
500    /**
501     * Retrieves the content search helper
502     * @return the content search helper
503     */
504    protected ContentSearchHelper _getContentSearchHelper()
505    {
506        if (_contentSearchHelper == null)
507        {
508            try
509            {
510                _contentSearchHelper = (ContentSearchHelper) __serviceManager.lookup(ContentSearchHelper.ROLE);
511            }
512            catch (ServiceException e)
513            {
514                throw new RuntimeException("Unable to lookup after the content search helper", e);
515            }
516        }
517        
518        return _contentSearchHelper;
519    }
520}