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