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            
314            contentTypes = new HashSet<>();
315            if (contentTypeId != null)
316            {
317                contentTypes.add(_getContentTypeExtensionPoint().getExtension(contentTypeId));
318                _getContentTypeExtensionPoint().getSubTypes(contentTypeId)
319                                               .stream()
320                                               .map(_getContentTypeExtensionPoint()::getExtension)
321                                               .forEach(contentTypes::add);
322            }
323        }
324        
325        Configuration wrap = _getCriterionDefinitionHelper().wrapCriterionConfiguration(_getRootCriterionConfiguration(), contentTypes);
326        
327        return wrap;
328    }
329    
330    /**
331     * Retrieves the root criterion configuration
332     * Used to wrap it for enumerator and widget parameters
333     * @return the root criterion configuration
334     */
335    protected Configuration _getRootCriterionConfiguration()
336    {
337        return new DefaultConfiguration("criterion");
338    }
339    
340    @SuppressWarnings("unchecked")
341    @Override
342    public Object convertQueryValue(Object value, Map<String, Object> contextualParameters)
343    {
344        return getReference() instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference
345            ? criterionDefinitionAwareReference.convertQueryValue(value, getReferencePath(), contextualParameters)
346            : super.convertQueryValue(value, contextualParameters);
347    }
348    
349    @SuppressWarnings("unchecked")
350    @Override
351    public Query getQuery(Object value, Operator operator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
352    {
353        if (operator != Operator.EXISTS && _getCriterionDefinitionHelper().isQueryValueEmpty(value))
354        {
355            return null;
356        }
357
358        ElementDefinition<T> reference = getReference();
359        Query query = null;
360        if (reference instanceof CriterionDefinitionAwareElementDefinition criterionDefinitionAwareReference)
361        {
362            // If value wanted is none, create query that search for contents with no value for the attribute
363            if (NONE_VALUE.equals(value))
364            {
365                query = new NotQuery(criterionDefinitionAwareReference.getQuery(value, Operator.EXISTS, language, contextualParameters));
366            }
367            else
368            {
369                query = criterionDefinitionAwareReference.getQuery(value, operator, language, contextualParameters);
370            }
371        }
372        else
373        {
374            boolean isMultipleOperandAnd = LogicalOperator.AND.equals(getMultipleOperandOperator());
375            CMSDataContext context = getQueryContext(language, contextualParameters);
376            
377            IndexableElementType referenceType = (IndexableElementType) reference.getType();
378            String queryFieldPath = _getContentSearchHelper().computeQueryFieldPath(reference);
379            
380            // If value wanted is none, create query that search for contents with no value for the attribute
381            if (NONE_VALUE.equals(value))
382            {
383                query = new NotQuery(referenceType.getDefaultQuery(value, queryFieldPath, Operator.EXISTS, isMultipleOperandAnd, context));
384            }
385            else
386            {
387                query = referenceType.getDefaultQuery(value, queryFieldPath, operator, isMultipleOperandAnd, context);
388            }
389        }
390        
391        List<String> queryJoinPaths = getJoinedPaths(contextualParameters);
392        if (query != null && !queryJoinPaths.isEmpty())
393        {
394            query = new JoinQuery(query, queryJoinPaths);
395        }
396        
397        return query;
398    }
399    
400    @Override
401    public String getSolrFacetFieldName(Map<String, Object> contextualParameters)
402    {
403        String solrFacetFieldName = super.getSolrFacetFieldName(contextualParameters);
404        
405        if (solrFacetFieldName == null && _isFacetable())
406        {
407            Set<String> contentTypeIds = getContentTypeIds(contextualParameters);
408            JoinedPaths joinedPaths =  _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds);
409            String fieldNameSuffix = _getFacetFieldNameSuffix();
410            solrFacetFieldName = joinedPaths.lastSegmentPrefix() + fieldNameSuffix;
411            setSolrFacetFieldName(solrFacetFieldName);
412        }
413
414        return solrFacetFieldName;
415    }
416    
417    /**
418     * Determine if the current criterion definition can be used as facet
419     * @return <code>true</code> if the current criterion definition can be used as facet, <code>false</code> otherwise
420     */
421    protected boolean _isFacetable()
422    {
423        ElementDefinition<T> reference = getReference();
424        return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition
425                ? indexationAwareElementDefinition.isFacetable()
426                : reference instanceof Property
427                    ? false
428                    : ((IndexableElementType) reference.getType()).isFacetable(DataContext.newInstance()
429                                                       .withModelItem(this));
430    }
431    
432    /**
433     * Retrieves the suffix of the facet field name
434     * @return the suffix of the facet field name
435     */
436    protected String _getFacetFieldNameSuffix()
437    {
438        ElementDefinition<T> reference = getReference();
439        return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition
440                ? indexationAwareElementDefinition.getSolrFacetFieldName()
441                : reference instanceof Property
442                    ? null
443                    : reference.getName() + ((IndexableElementType) reference.getType()).getFacetFieldSuffix(DataContext.newInstance()
444                                                                                                                        .withModelItem(reference));
445    }
446    
447    @Override
448    public List<String> getJoinedPaths(Map<String, Object> contextualParameters)
449    {
450        List<String> joinedPaths = super.getJoinedPaths(contextualParameters);
451        
452        if (joinedPaths.isEmpty())
453        {
454            Set<String> contentTypeIds = getContentTypeIds(contextualParameters);
455            JoinedPaths computedJoinedPaths = _getContentSearchHelper().computeJoinedPaths(getReferencePath(), contentTypeIds);
456            joinedPaths = computedJoinedPaths.joinedPaths();
457            setJoinedPaths(joinedPaths);
458        }
459
460        return joinedPaths;
461    }
462    
463    /**
464     * Retrieves The content type extension point
465     * @return The content type extension point
466     */
467    protected ContentTypeExtensionPoint _getContentTypeExtensionPoint()
468    {
469        if (_contentTypeExtensionPoint == null)
470        {
471            try
472            {
473                _contentTypeExtensionPoint = (ContentTypeExtensionPoint) __serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
474            }
475            catch (ServiceException e)
476            {
477                throw new RuntimeException("Unable to lookup after the content type extension point", e);
478            }
479        }
480        
481        return _contentTypeExtensionPoint;
482    }
483    
484    /**
485     * Retrieves The extension point containing all available criterion types
486     * @return The extension point containing all available criterion types
487     */
488    protected ModelItemTypeExtensionPoint _getCriterionTypeExtensionPoint()
489    {
490        if (_criterionTypeExtensionPoint == null)
491        {
492            try
493            {
494                _criterionTypeExtensionPoint = (ModelItemTypeExtensionPoint) __serviceManager.lookup(ModelItemTypeExtensionPoint.ROLE_CRITERION_DEFINITION);
495            }
496            catch (ServiceException e)
497            {
498                throw new RuntimeException("Unable to lookup after the extension point containing all available criterion types", e);
499            }
500        }
501        
502        return _criterionTypeExtensionPoint;
503    }
504    
505    /**
506     * Retrieves the content search helper
507     * @return the content search helper
508     */
509    protected ContentSearchHelper _getContentSearchHelper()
510    {
511        if (_contentSearchHelper == null)
512        {
513            try
514            {
515                _contentSearchHelper = (ContentSearchHelper) __serviceManager.lookup(ContentSearchHelper.ROLE);
516            }
517            catch (ServiceException e)
518            {
519                throw new RuntimeException("Unable to lookup after the content search helper", e);
520            }
521        }
522        
523        return _contentSearchHelper;
524    }
525}