001/*
002 *  Copyright 2018 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.runtime.model;
017
018import java.lang.reflect.Array;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.configuration.Configuration;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.cocoon.ProcessingException;
027import org.apache.commons.lang3.tuple.ImmutablePair;
028import org.apache.commons.lang3.tuple.Pair;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import org.ametys.runtime.config.Config;
033import org.ametys.runtime.config.DisableCondition;
034import org.ametys.runtime.config.DisableConditions;
035import org.ametys.runtime.i18n.I18nizableText;
036import org.ametys.runtime.model.exception.BadItemTypeException;
037import org.ametys.runtime.model.exception.UnknownTypeException;
038import org.ametys.runtime.model.type.DataContext;
039import org.ametys.runtime.model.type.ElementType;
040import org.ametys.runtime.model.type.ModelItemType;
041import org.ametys.runtime.parameter.Validator;
042import org.ametys.runtime.plugin.ExtensionPoint;
043
044/**
045 * The definition of a single model item (parameter, attribute)
046 * @param <T> Type of the element value
047 */
048public class ElementDefinition<T> extends AbstractModelItem
049{
050    /** config type for default values */
051    public static final String CONFIG_DEFAULT_VALUE_TYPE = "config";
052    
053    /** The definition logger */
054    protected Logger _logger = LoggerFactory.getLogger(this.getClass());
055    
056    private String _pluginName;
057    private ElementType<T> _type;
058    private String _widget;
059    private Map<String, I18nizableText> _widgetParams;
060    private Enumerator<T> _enumerator;
061    private String _customEnumerator;
062    private Configuration _enumeratorConfiguration;
063    private Validator _validator;
064    private String _customValidator;
065    private Configuration _validatorConfiguration;
066    private boolean _isMultiple;
067    private DisableConditions _disableConditions;
068    private List<Pair<String, Object>> _parsedDefaultValues;
069
070    /**
071     * Default constructor.
072     */
073    public ElementDefinition()
074    {
075        super();
076    }
077    
078    /**
079     * Constructor used to create simple models and items 
080     * @param name the name of the definition
081     * @param isMultiple the element multiple status
082     * @param type the type of the definition
083     */
084    public ElementDefinition(String name, boolean isMultiple, ElementType<T> type)
085    {
086        super(name);
087        _type = type;
088        _isMultiple = isMultiple;
089    }
090    
091    /**
092     * Constructor by copying an existing {@link ElementDefinition}.
093     * @param definitionToCopy The {@link ElementDefinition} to copy
094     */
095    public ElementDefinition(ElementDefinition<T> definitionToCopy)
096    {
097        super(definitionToCopy);
098        
099        // ElementDefinition
100        setPluginName(definitionToCopy.getPluginName());
101        setType(definitionToCopy.getType());
102        
103        // Widget
104        setWidget(definitionToCopy.getWidget());
105        setWidgetParameters(definitionToCopy.getWidgetParameters());
106        
107        // Enumerator
108        setEnumerator(definitionToCopy.getEnumerator());
109        setCustomEnumerator(definitionToCopy.getCustomEnumerator());
110        setEnumeratorConfiguration(definitionToCopy.getEnumeratorConfiguration());
111        
112        // Validator
113        setValidator(definitionToCopy.getValidator());
114        setCustomValidator(definitionToCopy.getCustomValidator());
115        setValidatorConfiguration(definitionToCopy.getValidatorConfiguration());
116        
117        // Other
118        setParsedDefaultValues(definitionToCopy._getParsedDefaultValues());
119        setMultiple(definitionToCopy.isMultiple());
120        setDisableConditions(definitionToCopy.getDisableConditions());
121    }
122
123    /**
124     * Retrieves the name of the plugin declaring this element.
125     * @return the plugin name.
126     */
127    public String getPluginName()
128    {
129        return _pluginName;
130    }
131
132    /**
133     * Set the name of the plugin declaring this element.
134     * @param pluginName the plugin name.
135     */
136    public void setPluginName(String pluginName)
137    {
138        _pluginName = pluginName;
139    }
140
141    @Override
142    public ElementType<T> getType()
143    {
144        return _type;
145    }
146
147    @SuppressWarnings("unchecked")
148    public void setType(ModelItemType type)
149    {
150        if (type instanceof ElementType)
151        {
152            _type = (ElementType<T>) type;
153        }
154        else
155        {
156            throw new IllegalArgumentException("Unable to set the type '" + type.getClass() + "' on the element type '" + getName() + "'");
157        }
158    }
159
160    /**
161     * Retrieves the widget to use for rendering.
162     * @return the widget or <code>null</code> if none is defined.
163     */
164    public String getWidget()
165    {
166        return _widget;
167    }
168
169    /**
170     * Set the widget.
171     * @param widget the widget.
172     */
173    public void setWidget(String widget)
174    {
175        _widget = widget;
176    }
177    
178    /**
179     * Get the widget's parameters
180     * @return the widget's parameters
181     */
182    public Map<String, I18nizableText> getWidgetParameters()
183    {
184        return _widgetParams;
185    }
186    
187    /**
188     * Set the widget's parameters
189     * @param params the parameters to set
190     */
191    public void setWidgetParameters (Map<String, I18nizableText> params)
192    {
193        _widgetParams = params;
194    }
195    
196    /**
197     * Retrieves the enumerator.
198     * @return the enumerator or <code>null</code> if none is defined.
199     */
200    public Enumerator<T> getEnumerator()
201    {
202        return _enumerator;
203    }
204
205    /**
206     * Set the enumerator.
207     * @param enumerator the enumerator.
208     */
209    public void setEnumerator(Enumerator<T> enumerator)
210    {
211        _enumerator = enumerator;
212    }
213    
214    /**
215     * Retrieves the custom enumerator's class name
216     * @return the custom enumerator's class name
217     */
218    public String getCustomEnumerator()
219    {
220        return _customEnumerator;
221    }
222    
223    /**
224     * Set the custom enumerator's class name
225     * @param customEnumerator the custom enumerator's class name
226     */
227    public void setCustomEnumerator(String customEnumerator)
228    {
229        this._customEnumerator = customEnumerator;
230    }
231
232    /**
233     * Retrieves the custom enumerator's configuration
234     * @return the custom enumerator's configuration
235     */
236    public Configuration getEnumeratorConfiguration()
237    {
238        return _enumeratorConfiguration;
239    }
240    
241    /**
242     * Set the custom enumerator's configuration
243     * @param enumeratorConfiguration the custom enumerator's configuration
244     */
245    public void setEnumeratorConfiguration(Configuration enumeratorConfiguration)
246    {
247        _enumeratorConfiguration = enumeratorConfiguration;
248    }
249
250    /**
251     * Retrieves the validator.
252     * @return the validator or <code>null</code> if none is defined.
253     */
254    public Validator getValidator()
255    {
256        return _validator;
257    }
258
259    /**
260     * Set the validator.
261     * @param validator the validator.
262     */
263    public void setValidator(Validator validator)
264    {
265        _validator = validator;
266    }
267    
268    /**
269     * Retrieves the custom validator's class name
270     * @return the custom validator's class name
271     */
272    public String getCustomValidator()
273    {
274        return _customValidator;
275    }
276    
277    /**
278     * Set the custom validator's class name
279     * @param customValidator the custom validator's class name
280     */
281    public void setCustomValidator(String customValidator)
282    {
283        this._customValidator = customValidator;
284    }
285    
286    /**
287     * Retrieves the custom validator's configuraiton
288     * @return the custom validator's configuration
289     */
290    public Configuration getValidatorConfiguration()
291    {
292        return _validatorConfiguration;
293    }
294    
295    /**
296     * Set the custom validator's configuration
297     * @param validatorConfiguration the custom validator's configuration
298     */
299    public void setValidatorConfiguration(Configuration validatorConfiguration)
300    {
301        _validatorConfiguration = validatorConfiguration;
302    }
303    
304    /**
305     * Retrieves the default value, as an object corresponding to the definition's type and cardinality
306     * Retrieves <code>null</code> if no default value is defined for this definition
307     * @param <X> The type of the default value
308     * @return the default value.
309     */
310    @SuppressWarnings("unchecked")
311    public <X> X getDefaultValue()
312    {
313        if (_parsedDefaultValues != null)
314        {
315            if (isMultiple())
316            {
317                Object defaultValuesAsArray = Array.newInstance(getType().getManagedClass(), _parsedDefaultValues.size());
318                int index = 0;
319                for (Pair<String, Object> parsedDefaultValue : _parsedDefaultValues)
320                {
321                    // Compute the parsed default value according to the default value type 
322                    T defaultValue = _getDefaultValue(parsedDefaultValue.getLeft(), parsedDefaultValue.getRight());
323                    Array.set(defaultValuesAsArray, index, defaultValue);
324                    index++;
325                }
326
327                return (X) defaultValuesAsArray;
328            }
329            else
330            {
331                if (!_parsedDefaultValues.isEmpty())
332                {
333                    if (_parsedDefaultValues.size() > 1)
334                    {
335                        _logger.warn("the data '" + this + "' is single, only the first declared default value will be used");
336                    }
337                    
338                    // Compute the parsed default value according to the default value type
339                    Pair<String, Object> parsedDefaultValue = _parsedDefaultValues.get(0);
340                    return (X) _getDefaultValue(parsedDefaultValue.getLeft(), parsedDefaultValue.getRight());
341                }
342                else 
343                {
344                    return null;
345                }
346            }
347        }
348        else
349        {
350            return null;
351        }
352    }
353    
354    /**
355     * Retrieves the default value from the parsed one, according to the type of the default value
356     * @param parsedDefaultValue the parsed default value (can be an {@link I18nizableText}, a config parameter name, ... depending on the default value type)
357     * @param defaultValueType the type of the default value
358     * @return the default value.
359     */
360    @SuppressWarnings("unchecked")
361    protected T _getDefaultValue(String defaultValueType, Object parsedDefaultValue)
362    {
363        if (CONFIG_DEFAULT_VALUE_TYPE.equals(defaultValueType))
364        {
365            assert parsedDefaultValue instanceof String;
366            return Config.getInstance().getValue((String) parsedDefaultValue);
367        }
368        else
369        {
370            return (T) parsedDefaultValue;
371        }
372    }
373    
374    /**
375     * Set the parsed default values.
376     * If the definition is not multiple, the list should contain only one element
377     * A parsed default value is described by its type and an object depending on the type 
378     * @param parsedDefaultValues the parsed default values.
379     */
380    public void setParsedDefaultValues(List<Pair<String, Object>> parsedDefaultValues)
381    {
382        _parsedDefaultValues = parsedDefaultValues;
383    }
384    
385    /**
386     * Set a default value to the definition
387     * The default value is single, classic default value
388     * @param defaultValue the default value to set
389     */
390    public void setDefaultValue(T defaultValue)
391    {
392        _parsedDefaultValues = List.of(new ImmutablePair<>(null, defaultValue));
393    }
394    
395    /**
396     * Retrieves the parsed default values
397     * @return the parsed default values
398     */
399    protected List<Pair<String, Object>> _getParsedDefaultValues()
400    {
401        return _parsedDefaultValues;
402    }
403
404    /**
405     * Test if the element is multiple.
406     * @return <code>true</code> if the metadata is multiple.
407     */
408    public boolean isMultiple()
409    {
410        return _isMultiple;
411    }
412    
413    /**
414     * Set the element multiple status.
415     * @param isMultiple the element multiple status.
416     */
417    public void setMultiple(boolean isMultiple)
418    {
419        _isMultiple = isMultiple;
420    }
421    
422    /**
423     * Retrieves the disable condition.
424     * @return the disable condition or <code>null</code> if none is defined.
425     */
426    public DisableConditions getDisableConditions()
427    {
428        return _disableConditions;
429    }
430
431    /**
432     * Sets the disable condition.
433     * @param disableConditions the disable condition.
434     */
435    public void setDisableConditions(DisableConditions disableConditions)
436    {
437        _disableConditions = disableConditions;
438    }
439
440    @Override
441    protected Map<String, Object> _toJSON(DefinitionContext context) throws ProcessingException
442    {
443        Map<String, Object> result = super._toJSON(context);
444        
445        result.put("plugin", getPluginName());
446        result.put("multiple", isMultiple());
447        
448        if (getType() != null)
449        {
450            result.put("type", getType().getId());
451            result.put("default-value", getType().valueToJSONForClient(getDefaultValue(), DataContext.newInstance()));
452        }
453        
454        
455        if (getValidator() != null)
456        {
457            result.put("validation", getValidator().getConfiguration());
458        }
459        
460        if (getEnumerator() != null)
461        {
462            List<Map<String, Object>> enumeration = new ArrayList<>();
463            
464            try
465            {
466                Map<T, I18nizableText> entries = getEnumerator().getTypedEntries();
467                for (Map.Entry<T, I18nizableText> entry : entries.entrySet())
468                {
469                    Map<String, Object> option = new HashMap<>();
470                    option.put("value", getType().valueToJSONForClient(entry.getKey(), DataContext.newInstance()));
471                    option.put("label", entry.getValue());
472                    enumeration.add(option);
473                }
474            }
475            catch (Exception e)
476            {
477                throw new ProcessingException("Unable to enumerate entries with enumerator: " + getEnumerator(), e);
478            }
479            
480            result.put("enumeration", enumeration);
481            result.put("enumerationConfig", getEnumerator().getConfiguration());
482        }
483        
484        result.put("widget", getWidget());
485        
486        Map<String, I18nizableText> widgetParameters = getWidgetParameters();
487        if (widgetParameters != null && !widgetParameters.isEmpty())
488        {
489            result.put("widget-params", widgetParameters);
490        }
491        
492        if (getDisableConditions() != null)
493        {
494            result.put("disableCondition", _disableConditionstoJSON(getDisableConditions()));
495        }
496        
497        return result;
498    }
499    
500    /**
501     * Converts the definition's disable conditions in a JSON map
502     * @param disableConditions the disable conditions to convert
503     * @return The definition's disable conditions as a JSON map
504     */
505    private Map<String, Object> _disableConditionstoJSON(DisableConditions disableConditions)
506    {
507        Map<String, Object> map = new HashMap<>();
508        
509        // Handle simple conditions
510        List<Map<String, String>> disableConditionList = new ArrayList<>();
511        map.put("condition", disableConditionList);
512        for (DisableCondition disableCondition : disableConditions.getConditions())
513        {
514            Map<String, String> disableConditionAsMap = _disableConditiontoJSON(disableCondition);
515            disableConditionList.add(disableConditionAsMap);
516        }
517
518        // Handle nested conditions
519        List<Map<String, Object>> disableConditionsList = new ArrayList<>();
520        map.put("conditions", disableConditionsList);
521        for (DisableConditions subDisableConditions : disableConditions.getSubConditions())
522        {
523            Map<String, Object> disableConditionsAsMap = _disableConditionstoJSON(subDisableConditions);
524            disableConditionsList.add(disableConditionsAsMap);
525        }
526        
527        // Handle type
528        map.put("type", disableConditions.getAssociationType().toString().toLowerCase());
529        
530        return map; 
531    }
532
533    private static Map<String, String> _disableConditiontoJSON(DisableCondition disableCondition)
534    {
535        Map<String, String> map = new HashMap<>();
536        map.put("id", disableCondition.getId());
537        map.put("operator", disableCondition.getOperator().toString().toLowerCase());
538        map.put("value", disableCondition.getValue());
539        return map;
540    }
541    
542    /**
543     * Creates an {@link ElementDefinition}
544     * @param name the definition's name
545     * @param isMultiple the definition's cardinality
546     * @param typeId the definition's type identifier
547     * @param availableTypesExtensionPoint the role of the extension point containing all available types for this {@link ElementDefinition}
548     * @return the created {@link ElementDefinition}
549     * @throws UnknownTypeException if the given type identifier is not available in the extension point
550     * @throws BadItemTypeException if the given type identifier can not be used for an {@link ElementDefinition}
551     * @throws ServiceException if an error occurs while getting the extension point of available types
552     */
553    @SuppressWarnings("unchecked")
554    public static ElementDefinition of(String name, boolean isMultiple, String typeId, String availableTypesExtensionPoint) throws UnknownTypeException, BadItemTypeException, ServiceException
555    {
556        ExtensionPoint<ModelItemType> availableTypes = (ExtensionPoint<ModelItemType>) __serviceManager.lookup(availableTypesExtensionPoint);
557        if (!availableTypes.hasExtension(typeId))
558        {
559            throw new UnknownTypeException("The type '" + typeId + "' (used for data '" + name + "') is not available for the given extension point.");
560        }
561        else
562        {
563            ModelItemType type = availableTypes.getExtension(typeId);
564            if (!(type instanceof ElementType))
565            {
566                throw new BadItemTypeException("The type '" + typeId + "' (used for data '" + name + "') can not be used for an element definition.");
567            }
568            else
569            {
570                return new ElementDefinition(name, isMultiple, (ElementType) type);
571            }
572        }
573    }
574}