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.impl;
017
018import java.util.HashSet;
019import java.util.Optional;
020import java.util.Set;
021
022import org.apache.avalon.framework.activity.Disposable;
023import org.apache.avalon.framework.component.Component;
024import org.apache.avalon.framework.configuration.Configurable;
025import org.apache.avalon.framework.configuration.Configuration;
026import org.apache.avalon.framework.configuration.ConfigurationException;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.commons.lang3.StringUtils;
034
035import org.ametys.cms.contenttype.ContentType;
036import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
037import org.ametys.cms.contenttype.ContentTypesHelper;
038import org.ametys.cms.data.type.ModelItemTypeExtensionPoint;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.search.content.ContentSearchHelper;
041import org.ametys.cms.search.model.CriterionDefinitionHelper;
042import org.ametys.cms.search.model.SearchModelCriterionDefinition;
043import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
044import org.ametys.cms.search.query.Query.LogicalOperator;
045import org.ametys.cms.search.query.Query.Operator;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.model.ElementDefinition;
048import org.ametys.runtime.model.ItemParserHelper;
049import org.ametys.runtime.model.ItemParserHelper.ConfigurationAndPluginName;
050import org.ametys.runtime.model.ModelHelper;
051import org.ametys.runtime.model.ModelItem;
052import org.ametys.runtime.parameter.DefaultValidator;
053import org.ametys.runtime.parameter.Validator;
054import org.ametys.runtime.plugin.component.PluginAware;
055import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
056
057/**
058 * Static implementation for {@link SearchModelCriterionDefinition} searching on a model item.
059 * @param <T> Type of the criterion value
060 */
061public class StaticReferencingSearchModelCriterionDefinition<T> extends ReferencingSearchModelCriterionDefinition<T> implements Component, Contextualizable, PluginAware, Serviceable, Configurable, Disposable
062{
063    /** The system property extension point */
064    protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
065    
066    /** The content types helper */
067    protected ContentTypesHelper _contentTypesHelper;
068    
069    /** The criterion root configuration */
070    protected Configuration _rootConfiguration;
071    
072    /** ComponentManager for {@link Validator}s. */
073    protected ThreadSafeComponentManager<Validator> _validatorManager;
074    
075    public void contextualize(Context context) throws ContextException
076    {
077        __context = context;
078    }
079    
080    public void setPluginInfo(String pluginName, String featureName, String id)
081    {
082        setPluginName(pluginName);
083    }
084    
085    public void service(ServiceManager manager) throws ServiceException
086    {
087        __serviceManager = manager;
088        
089        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
090        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
091        
092        _criterionDefinitionHelper = (CriterionDefinitionHelper) manager.lookup(CriterionDefinitionHelper.ROLE);
093        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
094        _criterionTypeExtensionPoint = (ModelItemTypeExtensionPoint) manager.lookup(ModelItemTypeExtensionPoint.ROLE_CRITERION_DEFINITION);
095        _contentSearchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
096    }
097    
098    @Override
099    public void configure(Configuration configuration) throws ConfigurationException
100    {
101        _rootConfiguration = configuration;
102        setName(_parseName(configuration));
103        
104        // First parse reference to have type of the criterion, used to parse some other data (default value, widget, ...)
105        setReferencePath(_parseReferencePath(configuration));
106        setReference(_parseReference(configuration));
107        
108        ConfigurationAndPluginName configurationAndPluginName = new ConfigurationAndPluginName(configuration, getPluginName());
109        setLabel(ItemParserHelper.parseI18nizableText(configurationAndPluginName, "label", (I18nizableText) null));
110        setDescription(ItemParserHelper.parseI18nizableText(configurationAndPluginName, "description", (I18nizableText) null));
111        
112        setMultiple(ItemParserHelper.parseMultiple(configuration));
113        setParsedDefaultValues(ItemParserHelper.parseDefaultValues(configuration, this, ItemParserHelper::parseDefaultValue));
114
115        setOperator(_parseOperator(configuration));
116        setMultipleOperandOperator(LogicalOperator.valueOf(configuration.getChild("multiple-operand").getValue("or").toUpperCase()));
117        
118        setWidget(ItemParserHelper.parseWidget(configuration));
119        setWidgetParameters(ItemParserHelper.parseWidgetParameters(configuration, getPluginName()));
120        
121        _validatorManager = new ThreadSafeComponentManager<>();
122        _validatorManager.setLogger(_logger);
123        _validatorManager.contextualize(__context);
124        _validatorManager.service(__serviceManager);
125        
126        Optional<String> validatorRole = _parseValidator(_validatorManager, configuration);
127        if (validatorRole.isPresent())
128        {
129            try
130            {
131                _validatorManager.initialize();
132                setValidator(_validatorManager.lookup(validatorRole.get()));
133            }
134            catch (Exception e)
135            {
136                throw new ConfigurationException("Unable to initialize the validator for criterion referencing '" + getReferencePath() + "'.", configuration, e);
137            }
138        }
139    }
140    
141    private String _parseName(Configuration configuration)
142    {
143        // return null when not set to use default value from ReferencingSearchModelCriterionDefinition
144        return configuration.getAttribute("name", null);
145    }
146    /**
147     * Parse the criterion reference path.
148     * @param configuration The criterion definition configuration.
149     * @return The parsed reference path
150     * @throws ConfigurationException If an error occurs.
151     */
152    protected String _parseReferencePath(Configuration configuration) throws ConfigurationException
153    {
154        String referencePath = configuration.getAttribute("ref", configuration.getChild("item").getAttribute("ref", null));
155        
156        if (referencePath == null)
157        {
158            throw new ConfigurationException("Unable to parse the reference of the criterion definition. A referencing criterion definition should specify its reference in an attribute 'ref'", configuration);
159        }
160        
161        return referencePath;
162    }
163    
164    /**
165     * Parse the criterion reference
166     * @param configuration the criterion definition configuration
167     * @return the definition of the reference
168     * @throws ConfigurationException If an error occurs
169     */
170    @SuppressWarnings("unchecked")
171    protected ElementDefinition<T> _parseReference(Configuration configuration) throws ConfigurationException
172    {
173        String referencePath = getReferencePath();
174        String[] referencePathSegments = StringUtils.split(referencePath, ModelItem.ITEM_PATH_SEPARATOR);
175        String lastReferencePathSegment = referencePathSegments[referencePathSegments.length - 1];
176        
177        if (_systemPropertyExtensionPoint.hasExtension(lastReferencePathSegment))
178        {
179            return _systemPropertyExtensionPoint.getExtension(lastReferencePathSegment);
180        }
181        
182        Set<ContentType> contentTypes = new HashSet<>();
183        for (Configuration contentTypeConf : configuration.getChild("contentTypes").getChildren("type"))
184        {
185            String contentTypeId = contentTypeConf.getAttribute("id");
186            if (_contentTypeExtensionPoint.hasExtension(contentTypeId))
187            {
188                contentTypes.add(_contentTypeExtensionPoint.getExtension(contentTypeId));
189            }
190            else
191            {
192                throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. The referenced content type '" + contentTypeId + "' does not exist", configuration);
193            }
194        }
195        
196        if (contentTypes.isEmpty())
197        {
198            if (Content.ATTRIBUTE_TITLE.equals(referencePath))
199            {
200                return (ElementDefinition<T>) _contentTypesHelper.getTitleAttributeDefinition();
201            }
202            else
203            {
204                throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. There is no provided content type, only title attribute can be referenced", configuration);
205            }
206        }
207        else
208        {
209            try
210            {
211                ModelItem modelItem = ModelHelper.getModelItem(referencePath, contentTypes);
212                if (modelItem instanceof ElementDefinition definition)
213                {
214                    return definition;
215                }
216                else
217                {
218                    throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. The path does not references an element definition but a group", configuration);
219                }
220            }
221            catch (Exception e)
222            {
223                throw new ConfigurationException("Unable to configure the criterion definition referencing '" + referencePath + "'. There is no model item at this path", configuration, e);
224            }
225        }
226    }
227    
228    /**
229     * Parse the criterion operator
230     * @param configuration the criterion definition configuration
231     * @return the operator
232     * @throws ConfigurationException If an error occurs
233     */
234    protected Operator _parseOperator(Configuration configuration) throws ConfigurationException
235    {
236        String operatorName = configuration.getChild("test-operator")
237                                           .getValue(null);
238        return operatorName != null ? Operator.fromName(operatorName) : null;
239    }
240    
241    /**
242     * Parse the criterion validator.
243     * @param validatorManager The validator manager.
244     * @param config The validator configuration.
245     * @return The optional role of the validator, or an empty optional if there is no validator
246     * @throws ConfigurationException If an error occurs.
247     */
248    @SuppressWarnings("unchecked")
249    protected Optional<String> _parseValidator(ThreadSafeComponentManager<Validator> validatorManager, Configuration config) throws ConfigurationException
250    {
251        Configuration validatorConfig = config.getChild("validation", false);
252        
253        if (validatorConfig != null)
254        {
255            String validatorClassName = StringUtils.defaultIfBlank(validatorConfig.getChild("custom-validator").getAttribute("class", StringUtils.EMPTY), DefaultValidator.class.getName());
256            
257            try
258            {
259                String validatorRole = "validator";
260                Class validatorClass = Class.forName(validatorClassName);
261                validatorManager.addComponent(getPluginName(), null, validatorRole, validatorClass, config);
262                return Optional.of(validatorRole);
263            }
264            catch (Exception e)
265            {
266                throw new ConfigurationException("Unable to instantiate validator for class: " + validatorClassName, e);
267            }
268        }
269        
270        return Optional.empty();
271    }
272    
273    public void dispose()
274    {
275        _validatorManager.dispose();
276        _validatorManager = null;
277    }
278    
279    @Override
280    protected Configuration _getRootCriterionConfiguration()
281    {
282        return _rootConfiguration;
283    }
284}