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