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.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Map.Entry;
023
024import org.apache.avalon.framework.component.ComponentException;
025import org.apache.avalon.framework.configuration.Configuration;
026import org.apache.avalon.framework.configuration.ConfigurationException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.commons.lang3.tuple.ImmutablePair;
029import org.apache.commons.lang3.tuple.Pair;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033import org.ametys.plugins.core.ui.util.ConfigurationHelper;
034import org.ametys.runtime.config.ConfigManager;
035import org.ametys.runtime.config.DisableCondition;
036import org.ametys.runtime.config.DisableConditions;
037import org.ametys.runtime.i18n.I18nizableText;
038import org.ametys.runtime.model.type.ElementType;
039import org.ametys.runtime.model.type.ModelItemType;
040import org.ametys.runtime.parameter.DefaultValidator;
041import org.ametys.runtime.parameter.Validator;
042import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
043import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
044
045/**
046 * {@link ElementDefinition} parser from an XML configuration.
047 */
048public class ElementDefinitionParser extends AbstractModelItemParser
049{
050    private static final Logger __LOGGER = LoggerFactory.getLogger(ElementDefinitionParser.class);
051    
052    private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
053    private ThreadSafeComponentManager<Validator> _validatorManager;
054    private final Map<ElementDefinition, String> _validatorsToLookup = new HashMap<>();
055    private final Map<ElementDefinition, String> _enumeratorsToLookup = new HashMap<>();
056    
057    /**
058     * Creates an element definition parser.
059     * @param modelItemTypeExtensionPoint the extension point to use to get available element types
060     * @param enumeratorManager the enumerator component manager.
061     * @param validatorManager the validator component manager.
062     */
063    public ElementDefinitionParser(AbstractThreadSafeComponentExtensionPoint<? extends ModelItemType> modelItemTypeExtensionPoint, ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager)
064    {
065        super(modelItemTypeExtensionPoint);
066        _enumeratorManager = enumeratorManager;
067        _validatorManager = validatorManager;
068    }
069    
070    @Override
071    @SuppressWarnings("unchecked")
072    public <T extends ModelItem> T parse(ServiceManager serviceManager, String pluginName, String catalog, Configuration definitionConfig, Model model, ModelItemGroup parent) throws ConfigurationException
073    {
074        ElementDefinition definition = (ElementDefinition) super.parse(serviceManager, pluginName, catalog, definitionConfig, model, parent);
075        
076        definition.setPluginName(pluginName);
077        definition.setParsedDefaultValues(_parseDefaultValues(definitionConfig, definition));
078        definition.setMultiple(_parseMultiple(definitionConfig));
079        
080        definition.setWidget(_parseWidget(definitionConfig));
081        definition.setWidgetParameters(_parseWidgetParameters(definitionConfig, pluginName));
082        
083        _parseAndSetEnumerator(pluginName, catalog, definition, definitionConfig);
084        _parseAndSetValidator(pluginName, definition, definitionConfig);
085        
086        definition.setDisableConditions(_parseDisableConditions(definitionConfig));
087        
088        return (T) definition;
089    }
090    
091    @Override
092    protected ElementDefinition _createModelItem(Configuration definitionConfig) throws ConfigurationException
093    {
094        return new ElementDefinition();
095    }
096
097    /**
098     * Parses the default values.
099     * @param definitionConfig the element definition configuration.
100     * @param definition the element definition.
101     * @return the default values and their types or <code>null</code> if none default value is defined.
102     * @throws ConfigurationException if the configuration is not valid.
103     */
104    protected List<Pair<String, Object>> _parseDefaultValues(Configuration definitionConfig, ElementDefinition definition) throws ConfigurationException
105    {
106        Configuration[] defaultValueConfigs = definitionConfig.getChildren("default-value");
107        if (defaultValueConfigs.length > 0)
108        {
109            List<Pair<String, Object>> defaultValues = new ArrayList<>();
110            
111            for (Configuration defaultValueConfig : defaultValueConfigs)
112            {
113                String defaultValueType = defaultValueConfig.getAttribute("type", null);
114                Object defaultValue = _parseDefaultValue(defaultValueConfig, definition, defaultValueType);
115                defaultValues.add(new ImmutablePair<>(defaultValueType, defaultValue));
116            }
117            
118            return defaultValues;
119        }
120        else
121        {
122            return null;
123        }
124    }
125    
126    /**
127     * Parses the default value.
128     * @param defaultValueConfig the default value configuration.
129     * @param definition the element definition.
130     * @param defaultValueType the type of the default value
131     * @return the default value or <code>null</code> if none default value is defined.
132     * @throws ConfigurationException if the configuration is not valid.
133     */
134    protected Object _parseDefaultValue(Configuration defaultValueConfig, ElementDefinition definition, String defaultValueType) throws ConfigurationException
135    {
136        ElementType type = definition.getType();
137        if (defaultValueType != null)
138        {
139            if (ElementDefinition.CONFIG_DEFAULT_VALUE_TYPE.equals(defaultValueType))
140            {
141                String configParamName = defaultValueConfig.getValue();
142                if (ConfigManager.getInstance().hasModelItem(configParamName))
143                {
144                    ModelItem configParamDefinition = ConfigManager.getInstance().getModelItem(configParamName);
145                    String configParamTypeId = configParamDefinition.getType().getId();
146                    if (configParamTypeId.equals(type.getId()))
147                    {
148                        return configParamName;
149                    }
150                    else
151                    {
152                        throw new ConfigurationException("The configuration parameter '" + configParamName + " (" + configParamTypeId + ")' cannot be used as default value for item '" + definition.getPath() + " (" + type.getId() + ")': types are not the same.", defaultValueConfig);
153                    }
154                }
155                else
156                {
157                    throw new ConfigurationException("The configuration parameter '" + configParamName + "' does not exist, it cannot be used as default value for item '" + definition.getPath() + " (" + type.getId() + ")'.", defaultValueConfig);
158                }
159            }
160            else
161            {
162                throw new ConfigurationException("The type '" + defaultValueType + "' is not available for the default value of item '" + definition.getPath() + " (" + type.getId() + ")'.", defaultValueConfig);
163            }
164        }
165        else
166        {
167            return type.parseConfiguration(defaultValueConfig);
168        }
169    }
170    
171    /**
172     * Parses the multiple attribute.
173     * @param definitionConfig the element definition configuration to use.
174     * @return the true if the element is multiple, false otherwise.
175     * @throws ConfigurationException if the configuration is not valid.
176     */
177    protected Boolean _parseMultiple(Configuration definitionConfig) throws ConfigurationException
178    {
179        return definitionConfig.getAttributeAsBoolean("multiple", false);
180    }
181    
182    /**
183     * Parses the widget.
184     * @param definitionConfig the element definition configuration to use.
185     * @return the widget or <code>null</code> if none defined.
186     * @throws ConfigurationException if the configuration is not valid.
187     */
188    protected String _parseWidget(Configuration definitionConfig) throws ConfigurationException
189    {
190        return definitionConfig.getChild("widget").getValue(null);
191    }
192    
193    /**
194     * Parses the widget's parameters
195     * @param definitionConfig the parameter ele;ent definition to use.
196     * @param pluginName the current plugin name.
197     * @return the widget's parameters in a Map
198     * @throws ConfigurationException if the configuration is not valid.
199     */
200    protected Map<String, I18nizableText> _parseWidgetParameters(Configuration definitionConfig, String pluginName) throws ConfigurationException
201    {
202        Map<String, I18nizableText> widgetParams = new HashMap<>();
203        
204        Configuration widgetParamsConfig = definitionConfig.getChild("widget-params", false);
205        if (widgetParamsConfig != null)
206        {
207            Map<String, Object> parsedParams = ConfigurationHelper.parsePluginParameters(widgetParamsConfig, pluginName, __LOGGER);
208            
209            for (Entry<String, Object> param : parsedParams.entrySet())
210            {
211                String paramName = param.getKey();
212                Object value = param.getValue();
213                if (value instanceof I18nizableText)
214                {
215                    widgetParams.put(paramName, (I18nizableText) value);
216                }
217                else if (value instanceof String)
218                {
219                    widgetParams.put(paramName, new I18nizableText((String) value));
220                }
221                else
222                {
223                    __LOGGER.warn("Widget parameter '{}' at location {} is of type [{}] which is not supported. It will be ignored.", paramName, definitionConfig.getLocation(), value.getClass());
224                }
225            }
226        }
227        
228        return widgetParams;
229    }
230
231    /**
232     * Parses the enumerator.
233     * @param pluginName the plugin name.
234     * @param catalog the catalog
235     * @param definition the element definition.
236     * @param definitionConfig the element definition configuration.
237     * @throws ConfigurationException if the configuration is not valid.
238     */
239    @SuppressWarnings("unchecked")
240    protected void _parseAndSetEnumerator(String pluginName, String catalog, ElementDefinition definition, Configuration definitionConfig) throws ConfigurationException
241    {
242        Configuration enumeratorConfig = definitionConfig.getChild("enumeration", false);
243        
244        if (enumeratorConfig != null)
245        {
246            Configuration customEnumerator = enumeratorConfig.getChild("custom-enumerator", false);
247            
248            if (customEnumerator != null)
249            {
250                final String enumeratorClassName = customEnumerator.getAttribute("class");
251                final String enumeratorRole = definition.getPath();
252                
253                try
254                {
255                    Class enumeratorClass = Class.forName(enumeratorClassName);
256                    _enumeratorManager.addComponent(pluginName, null, enumeratorRole, enumeratorClass, definitionConfig);
257                }
258                catch (Exception e)
259                {
260                    throw new ConfigurationException("Unable to instantiate enumerator for class: " + enumeratorClassName, e);
261                }
262
263                // This enumerator will be affected later when enumeratorManager
264                // will be initialized in lookupComponents() call
265                _enumeratorsToLookup.put(definition, enumeratorRole);
266                    
267                // Add the custom enumerator information to the element definition
268                definition.setCustomEnumerator(enumeratorClassName);
269                definition.setEnumeratorConfiguration(customEnumerator);
270            }
271            else
272            {
273                StaticEnumerator staticEnumerator = new StaticEnumerator();
274                
275                for (Configuration entryConfig : enumeratorConfig.getChildren("entry"))
276                {
277                    Configuration valueConfiguration = entryConfig.getChild("value");
278                    Object value = definition.getType().parseConfiguration(valueConfiguration);
279                    I18nizableText label = null;
280                    
281                    if (entryConfig.getChild("label", false) != null)
282                    {
283                        label = _parseI18nizableText(entryConfig, catalog, "label");
284                    }
285                    
286                    staticEnumerator.add(label, value);
287                }
288                
289                definition.setEnumerator(staticEnumerator);
290            }
291        }
292    }
293
294    /**
295     * Parses the validator.
296     * @param pluginName the plugin name.
297     * @param definition the element definition.
298     * @param definitionConfig the element definition configuration.
299     * @throws ConfigurationException if the configuration is not valid.
300     */
301    @SuppressWarnings("unchecked")
302    protected void _parseAndSetValidator(String pluginName, ElementDefinition definition, Configuration definitionConfig) throws ConfigurationException
303    {
304        Configuration validatorConfig = definitionConfig.getChild("validation", false);
305        
306        if (validatorConfig != null)
307        {
308            Configuration customValidator = validatorConfig.getChild("custom-validator", false);
309            String validatorClassName;
310
311            if (customValidator != null)
312            {
313                validatorClassName = customValidator.getAttribute("class");
314                
315                // Add the custom validator information to the element definition
316                definition.setCustomValidator(validatorClassName);
317                definition.setValidatorConfiguration(customValidator);
318            }
319            else
320            {
321                validatorClassName = DefaultValidator.class.getName();
322            }
323            
324            final String validatorRole = definition.getPath();
325            
326            try
327            {
328                Class validatorClass = Class.forName(validatorClassName);
329                _validatorManager.addComponent(pluginName, null, validatorRole, validatorClass, definitionConfig);
330            }
331            catch (Exception e)
332            {
333                throw new ConfigurationException("Unable to instantiate validator for class: " + validatorClassName, e);
334            }
335
336            // Will be affected later when validatorManager will be initialized
337            // in lookupComponents() call
338            _validatorsToLookup.put(definition, validatorRole);
339        }
340    }
341    
342    /**
343     * Parses the disable condition.
344     * @param definitionConfiguration the configuration of the disable condition
345     * @return result the parsed disable condition to be converted in JSON
346     * @throws ConfigurationException if an error occurred
347     */
348    protected DisableConditions _parseDisableConditions(Configuration definitionConfiguration) throws ConfigurationException
349    {
350        Configuration disableConditionConfiguration = definitionConfiguration.getChild("disable-conditions", false);
351        DisableConditions conditions = null;
352        
353        if (disableConditionConfiguration != null)
354        {
355            conditions = new DisableConditions();
356
357            Configuration[] conditionsConfiguration = disableConditionConfiguration.getChildren();
358            for (Configuration conditionConfiguration : conditionsConfiguration)
359            {
360                String tagName = conditionConfiguration.getName();
361                
362                // Recursive case
363                if (tagName.equals("conditions"))
364                {
365                    conditions.getSubConditions().add(_parseDisableConditions(conditionConfiguration));
366                }
367                else if (tagName.equals("condition"))
368                {
369                    String id = conditionConfiguration.getAttribute("id");
370                    DisableCondition.OPERATOR operator = DisableCondition.OPERATOR.valueOf(conditionConfiguration.getAttribute("operator", "eq").toUpperCase());
371                    String value = conditionConfiguration.getValue("");
372                    
373                    
374                    DisableCondition condition = new DisableCondition(id, operator, value);
375                    conditions.getConditions().add(condition);
376                }
377            }
378            
379            conditions.setAssociation(DisableConditions.ASSOCIATION_TYPE.valueOf(disableConditionConfiguration.getAttribute("type", "and").toUpperCase()));
380        }
381        
382        return conditions;
383    }
384
385    /**
386     * Retrieves local validators and enumerators components and set them into
387     * previous parsed element definition.
388     * @throws Exception if an error occurs.
389     */
390    @SuppressWarnings("unchecked")
391    public void lookupComponents() throws Exception
392    {
393        _validatorManager.initialize();
394        _enumeratorManager.initialize();
395        
396        for (Map.Entry<ElementDefinition, String> entry : _validatorsToLookup.entrySet())
397        {
398            ElementDefinition definition = entry.getKey();
399            String validatorRole = entry.getValue();
400            
401            try
402            {
403                definition.setValidator(_validatorManager.lookup(validatorRole));
404            }
405            catch (ComponentException e)
406            {
407                throw new Exception("Unable to lookup validator role: '" + validatorRole + "' for parameter: " + definition, e);
408            }
409        }
410        
411        for (Map.Entry<ElementDefinition, String> entry : _enumeratorsToLookup.entrySet())
412        {
413            ElementDefinition definition = entry.getKey();
414            String enumeratorRole = entry.getValue();
415            
416            try
417            {
418                definition.setEnumerator(_enumeratorManager.lookup(enumeratorRole));
419            }
420            catch (ComponentException e)
421            {
422                throw new Exception("Unable to lookup enumerator role: '" + enumeratorRole + "' for parameter: " + definition, e);
423            }
424        }
425    }
426}