001/*
002 *  Copyright 2013 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.ui.model.impl;
017
018import java.util.Arrays;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.activity.Disposable;
027import org.apache.avalon.framework.configuration.Configurable;
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.commons.lang3.StringUtils;
037import org.slf4j.Logger;
038
039import org.ametys.cms.content.ContentHelper;
040import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
041import org.ametys.cms.contenttype.MetadataType;
042import org.ametys.cms.model.ContentElementDefinition;
043import org.ametys.cms.search.SearchField;
044import org.ametys.cms.search.ui.model.SearchUICriterion;
045import org.ametys.cms.search.ui.model.SearchUIModel;
046import org.ametys.plugins.core.ui.util.ConfigurationHelper;
047import org.ametys.runtime.i18n.I18nizableText;
048import org.ametys.runtime.model.ElementDefinition;
049import org.ametys.runtime.model.Enumerator;
050import org.ametys.runtime.model.StaticEnumerator;
051import org.ametys.runtime.parameter.DefaultValidator;
052import org.ametys.runtime.parameter.Parameter;
053import org.ametys.runtime.parameter.Validator;
054import org.ametys.runtime.plugin.component.LogEnabled;
055import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
056
057/**
058 * This class represents a search criterion of a {@link SearchUIModel}.
059 */
060public abstract class AbstractSearchUICriterion extends Parameter<MetadataType> implements SearchUICriterion, Contextualizable, Serviceable, Configurable, Disposable, LogEnabled
061{
062    /** The content type extension point. */
063    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
064    
065    /** The content type extension point. */
066    protected ContentHelper _contentHelper;
067    
068    /** ComponentManager for {@link Enumerator}s. */
069    protected ThreadSafeComponentManager<Enumerator> _enumeratorManager;
070    
071    /** The service manager */
072    protected ServiceManager _manager;
073    
074    /** the logger */
075    protected Logger _logger;
076    
077    /** The avalon context */
078    protected Context _context;
079
080    private String _onInitClassName;
081    private String _onSubmitClassName;
082    private String _onChangeClassName;
083    private boolean _hidden;
084    private boolean _multiple;
085    private I18nizableText _group;
086    
087    private String _contentTypeId;
088    
089    @Override
090    public void contextualize(Context context) throws ContextException
091    {
092        _context = context;
093    }
094    
095    @Override
096    public void service(ServiceManager manager) throws ServiceException
097    {
098        _manager = manager;
099        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
100        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
101    }
102    
103    @Override
104    public void configure(Configuration configuration) throws ConfigurationException
105    {
106        configureId(configuration);
107        
108        configureLabelsAndGroup(configuration);
109        configureUIProperties(configuration);
110        configureValues(configuration);
111        
112        _enumeratorManager = new ThreadSafeComponentManager<>();
113        _enumeratorManager.setLogger(_logger);
114        _enumeratorManager.contextualize(_context);
115        _enumeratorManager.service(_manager);
116    }
117    
118    public void dispose()
119    {
120        _enumeratorManager.dispose();
121        _enumeratorManager = null;
122    }
123    
124    @Override
125    public void setLogger(Logger logger)
126    {
127        _logger = logger;
128    }
129    
130    /**
131     * Configure the criterion ID.
132     * @param configuration The search criterion configuration.
133     * @throws ConfigurationException If an error occurs.
134     */
135    protected void configureId(Configuration configuration) throws ConfigurationException
136    {
137        setId(configuration.getAttribute("id", null));
138    }
139    
140    /**
141     * Configure the labels and group.
142     * @param configuration The search criterion configuration.
143     * @throws ConfigurationException If an error occurs.
144     */
145    protected void configureLabelsAndGroup(Configuration configuration) throws ConfigurationException
146    {
147        setGroup(_configureI18nizableText(configuration.getChild("group", false), null));
148        setLabel(_configureI18nizableText(configuration.getChild("label", false), new I18nizableText("")));
149        setDescription(_configureI18nizableText(configuration.getChild("description", false), new I18nizableText("")));
150    }
151    
152    /**
153     * Configure the default value.
154     * @param configuration The search criterion configuration.
155     * @throws ConfigurationException If an error occurs.
156     */
157    protected void configureValues(Configuration configuration) throws ConfigurationException
158    {
159        // The default value can be empty.
160        Configuration[] defaultValueConfs = configuration.getChildren("default-value");
161        if (defaultValueConfs.length == 1)
162        {
163            setDefaultValue(ConfigurationHelper.parseObject(defaultValueConfs[0], ""));
164        }
165        else if (defaultValueConfs.length > 1)
166        {
167            // Make the default value a list
168            List<Object> collection = Arrays.asList(defaultValueConfs).stream()
169                    .map(conf -> ConfigurationHelper.parseObject(conf, ""))
170                    .collect(Collectors.toList());
171            setDefaultValue(collection);
172        }
173    }
174    
175    /**
176     * Configure the standard UI properties (hidden, init class, change class, submit class).
177     * @param configuration The search criterion configuration.
178     * @throws ConfigurationException If an error occurs.
179     */
180    protected void configureUIProperties(Configuration configuration) throws ConfigurationException
181    {
182        setHidden(configuration.getAttributeAsBoolean("hidden", false));
183        setInitClassName(configuration.getChild("oninit").getValue(null));
184        setChangeClassName(configuration.getChild("onchange").getValue(null));
185        setSubmitClassName(configuration.getChild("onsubmit").getValue(null));
186    }
187    
188    /**
189     * Configure enumerator of the criterion
190     * @param configuration The search criterion configuration.
191     * @param definition The definition of the criterion
192     * @return the enumerator
193     * @throws ConfigurationException if an error occurs
194     */
195    protected org.ametys.runtime.parameter.Enumerator configureEnumerator(Configuration configuration, ElementDefinition definition) throws ConfigurationException
196    {
197        return configureEnumerator(configuration, definition, configuration);
198    }
199    
200    /**
201     * Configure enumerator of the criterion
202     * @param configuration The search criterion configuration.
203     * @param definition The definition of the criterion
204     * @param defaultEnumeratorConfig The configuration for property's widget params
205     * @return the enumerator
206     * @throws ConfigurationException if an error occurs
207     */
208    @SuppressWarnings("unchecked")
209    protected org.ametys.runtime.parameter.Enumerator configureEnumerator(Configuration configuration, ElementDefinition definition, Configuration defaultEnumeratorConfig) throws ConfigurationException
210    {
211        Enumerator enumerator = definition.getCriterionEnumerator(defaultEnumeratorConfig, _enumeratorManager);
212        org.ametys.runtime.parameter.Enumerator oldAPIEnumerator = Optional.ofNullable(enumerator)
213                       .filter(org.ametys.runtime.parameter.Enumerator.class::isInstance)
214                       .map(org.ametys.runtime.parameter.Enumerator.class::cast)
215                       .orElse(null);
216        
217        if (enumerator instanceof StaticEnumerator)
218        {
219            oldAPIEnumerator = new org.ametys.runtime.parameter.StaticEnumerator();
220            
221            try
222            {
223                for (Map.Entry<? extends Object, I18nizableText> entry : ((Map<Object, I18nizableText>) (Object) enumerator.getTypedEntries()).entrySet())
224                {
225                    ((org.ametys.runtime.parameter.StaticEnumerator) oldAPIEnumerator).add(entry.getValue(), entry.getKey().toString());
226                }
227            }
228            catch (Exception e)
229            {
230                throw new ConfigurationException("Unable to get entries of the static enumerator of criterion '" + definition.getPath() + "'", configuration, e);
231            }
232        }
233        
234        return oldAPIEnumerator;
235    }
236    
237    /**
238     * Configure widget of the criterion
239     * @param configuration The search criterion configuration.
240     * @param definition The definition of the criterion
241     * @return the widget
242     */
243    protected String configureWidget(Configuration configuration, ElementDefinition definition)
244    {
245        String defaultWidget = definition.getCriterionWidget();
246        return configureWidget(configuration, defaultWidget, MetadataType.fromModelItemType(definition.getType()));
247    }
248    
249    /**
250     * Configure widget of the criterion
251     * @param configuration The search criterion configuration.
252     * @param defaultWidget The default widget to use if not present in configuration. If {@link MetadataType#RICH_TEXT} or {@link MetadataType#MULTILINGUAL_STRING}, it will be forced.
253     * @param type the type which must be supported by the widget
254     * @return the widget
255     */
256    protected String configureWidget(Configuration configuration, String defaultWidget, MetadataType type)
257    {
258        String realDefaultWidget = defaultWidget;
259        if ("edition.textarea".equals(realDefaultWidget))
260        {
261            realDefaultWidget = null;
262        }
263        else if (type == MetadataType.RICH_TEXT || type == MetadataType.MULTILINGUAL_STRING)
264        {
265            realDefaultWidget = "edition.textfield"; // Force simple text field
266        }
267        else if (realDefaultWidget == null && type == MetadataType.BOOLEAN)
268        {
269            realDefaultWidget = "edition.boolean-combobox";
270        }
271        
272        return configuration.getChild("widget").getValue(realDefaultWidget);
273    }
274    
275    /**
276     * Configure widget parameters of the criterion
277     * @param configuration The search criterion configuration.
278     * @param defaultParams The default widget parameters to override with configuration
279     * @param type the type which must be supported by the widget. If {@link MetadataType#CONTENT} or {@link MetadataType#SUB_CONTENT}, some parameters will be forced.
280     * @param contentTypeId For {@link MetadataType#CONTENT} or {@link MetadataType#SUB_CONTENT} types only. The id of the content type.
281     * @return the widget parameters
282     * @throws ConfigurationException If an error occurs.
283     */
284    protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, Map<String, I18nizableText> defaultParams, MetadataType type, String contentTypeId) throws ConfigurationException
285    {
286        Configuration widgetParamsConfig = configuration.getChild("widget-params");
287        Map<String, I18nizableText> widgetParams = new HashMap<>(Optional.ofNullable(defaultParams).orElseGet(Collections::emptyMap));
288        
289        if (widgetParamsConfig != null)
290        {
291            // Overriden params
292            Configuration[] params = widgetParamsConfig.getChildren();
293            for (Configuration paramConfig : params)
294            {
295                String name = "param".equals(paramConfig.getName()) ? paramConfig.getAttribute("name") : paramConfig.getName();
296                widgetParams.put(name, I18nizableText.parseI18nizableText(paramConfig, null));
297            }
298        }
299        
300        // Force some param for types CONTENT and SUB_CONTENT
301        if (type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT)
302        {
303            setContentTypeId(contentTypeId);
304            if (contentTypeId != null)
305            {
306                widgetParams.put("contentType", new I18nizableText(contentTypeId));
307            }
308            
309            // Override the widget parameters to disable search and creation
310            widgetParams.put("allowCreation", new I18nizableText("false"));
311            widgetParams.put("allowSearch", new I18nizableText("false"));
312        }
313
314        return widgetParams;
315    }
316    
317    /**
318     * Configure widget parameters of the criterion
319     * @param configuration The search criterion configuration.
320     * @param definition The definition of the criterion's element
321     * @return the widget parameters
322     * @throws ConfigurationException If an error occurs.
323     */
324    protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, ElementDefinition definition) throws ConfigurationException
325    {
326        return configureWidgetParameters(configuration, definition, configuration);
327    }
328    
329    /**
330     * Configure widget parameters of the criterion
331     * @param configuration The search criterion configuration.
332     * @param definition The definition of the criterion's element
333     * @param defaultWidgetParamsConfig The configuration for property's widget params
334     * @return the widget parameters
335     * @throws ConfigurationException If an error occurs.
336     */
337    protected Map<String, I18nizableText> configureWidgetParameters(Configuration configuration, ElementDefinition definition, Configuration defaultWidgetParamsConfig) throws ConfigurationException
338    {
339        Map<String, I18nizableText> widgetParams = new HashMap<>();
340        Map<String, I18nizableText> criterionWidgetParameters = definition.getCriterionWidgetParameters(defaultWidgetParamsConfig);
341        if (criterionWidgetParameters != null)
342        {
343            widgetParams.putAll(criterionWidgetParameters);
344        }
345        
346        // Force some params for type CONTENT
347        if (definition instanceof ContentElementDefinition contentElementDef)
348        {
349            String contentTypeId = contentElementDef.getContentTypeId();
350            if (StringUtils.isNotEmpty(contentTypeId))
351            {
352                setContentTypeId(contentTypeId);
353                widgetParams.put("contentType", new I18nizableText(contentTypeId));
354            }
355            
356            // Override the widget parameters to disable search and creation
357            widgetParams.put("allowCreation", new I18nizableText("false"));
358            widgetParams.put("allowSearch", new I18nizableText("false"));
359        }
360        
361        Configuration widgetParamsConfig = configuration.getChild("widget-params");
362        if (widgetParamsConfig != null)
363        {
364            // Overridden params
365            Configuration[] params = widgetParamsConfig.getChildren();
366            for (Configuration paramConfig : params)
367            {
368                String name = "param".equals(paramConfig.getName()) ? paramConfig.getAttribute("name") : paramConfig.getName();
369                widgetParams.put(name, I18nizableText.parseI18nizableText(paramConfig, null));
370            }
371        }
372        
373        return widgetParams;
374    }
375    
376    /**
377     * Get the JS class name to execute on 'init' event
378     * @return the JS class name to execute on 'init' event
379     */
380    public String getInitClassName()
381    {
382        return _onInitClassName;
383    }
384
385    /**
386     * Set the JS class name to execute on 'init' event
387     * @param className the JS class name 
388     */
389    public void setInitClassName(String className)
390    {
391        this._onInitClassName = className;
392    }
393
394    /**
395     * Get the JS class name to execute on 'submit' event
396     * @return the JS class name to execute on 'submit' event
397     */
398    public String getSubmitClassName()
399    {
400        return _onSubmitClassName;
401    }
402
403    /**
404     * Set the JS class name to execute on 'submit' event
405     * @param className the JS class name 
406     */
407    public void setSubmitClassName(String className)
408    {
409        this._onSubmitClassName = className;
410    }
411
412    /**
413     * Get the JS class name to execute on 'change' event
414     * @return the JS class name to execute on 'change' event
415     */
416    public String getChangeClassName()
417    {
418        return _onChangeClassName;
419    }
420
421    /**
422     * Set the JS class name to execute on 'change' event
423     * @param className the JS class name 
424     */
425    public void setChangeClassName(String className)
426    {
427        this._onChangeClassName = className;
428    }
429    
430    /**
431     * Get the group of the search criteria
432     * @return <code>null</code> if the search criteria does not belong to any group, the name of the group otherwise
433     */
434    public I18nizableText getGroup()
435    {
436        return _group;
437    }
438    
439    /**
440     * Set the group of the search criteria
441     * @param group the group this search criteria will be added to
442     */
443    public void setGroup(I18nizableText group)
444    {
445        _group = group;
446    }
447    
448    /**
449     * Determines if the criteria is hidden
450     * @return <code>true</code> if the criteria is hidden
451     */
452    public boolean isHidden()
453    {
454        return _hidden;
455    }
456    
457    /**
458     * Set the hidden property of the criteria
459     * @param hidden true to hide the search criteria
460     */
461    public void setHidden (boolean hidden)
462    {
463        this._hidden = hidden;
464    }
465    
466    /**
467     * Set the multiple property
468     * @param multiple the multiple property
469     */
470    public void setMultiple (boolean multiple)
471    {
472        this._multiple = multiple;
473    }
474    
475    /**
476     * Determines if the column value is multiple
477     * @return <code>true</code> if the value is multiple
478     */
479    @Override
480    public boolean isMultiple ()
481    {
482        return this._multiple;
483    }
484    
485    /**
486     * Get the content type ID (only when the search criteria is of type CONTENT).
487     * @return the content type ID.
488     */
489    public String getContentTypeId()
490    {
491        return this._contentTypeId;
492    }
493    
494    /**
495     * Set the content type ID (only when the search criteria is of type CONTENT).
496     * @param contentTypeId the content type ID. 
497     */
498    public void setContentTypeId(String contentTypeId)
499    {
500        this._contentTypeId = contentTypeId;
501    }
502    
503    @Override
504    public SearchField getSearchField()
505    {
506        // Override to provide a specific implementation.
507        return null;
508    }
509    
510    /**
511     * Initialize the validator.
512     * @param validatorManager The validator manager.
513     * @param pluginName The plugin name.
514     * @param role The validator role.
515     * @param config The validator configuration.
516     * @return true if the validator was successfully added, false otherwise.
517     * @throws ConfigurationException If an error occurs.
518     */
519    @SuppressWarnings("unchecked")
520    protected boolean _initializeValidator(ThreadSafeComponentManager<Validator> validatorManager, String pluginName, String role, Configuration config) throws ConfigurationException
521    {
522        Configuration validatorConfig = config.getChild("validation", false);
523        
524        if (validatorConfig != null)
525        {
526            String validatorClassName = StringUtils.defaultIfBlank(validatorConfig.getChild("custom-validator").getAttribute("class", ""), DefaultValidator.class.getName());
527            
528            try
529            {
530                Class validatorClass = Class.forName(validatorClassName);
531                validatorManager.addComponent(pluginName, null, role, validatorClass, config);
532                return true;
533            }
534            catch (Exception e)
535            {
536                throw new ConfigurationException("Unable to instantiate validator for class: " + validatorClassName, e);
537            }
538        }
539        
540        return false;
541    }
542    
543    /**
544     * Configure an i18nizable text
545     * @param config The Configuration.
546     * @param defaultValue The default value as an I18nizableText.
547     * @return The i18nizable text
548     */
549    protected I18nizableText _configureI18nizableText(Configuration config, I18nizableText defaultValue)
550    {
551        if (config != null)
552        {
553            return I18nizableText.parseI18nizableText(config, null, "");
554        }
555        else
556        {
557            return defaultValue;
558        }
559    }
560}