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