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