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