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