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