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.HashMap;
019import java.util.Map;
020import java.util.Map.Entry;
021
022import org.apache.avalon.framework.configuration.Configuration;
023import org.apache.avalon.framework.configuration.ConfigurationException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.commons.lang3.StringUtils;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import org.ametys.plugins.core.ui.util.ConfigurationHelper;
030import org.ametys.runtime.config.DisableCondition;
031import org.ametys.runtime.config.DisableConditions;
032import org.ametys.runtime.i18n.I18nizableText;
033import org.ametys.runtime.model.exception.UnknownTypeException;
034import org.ametys.runtime.model.type.ModelItemType;
035import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
036
037/**
038 * {@link ModelItem} parser from an XML configuration.
039 */
040public abstract class AbstractModelItemParser
041{
042    private static final Logger __LOGGER = LoggerFactory.getLogger(AbstractModelItemParser.class);
043
044    /** The extension point to use to get available model items types */
045    protected AbstractThreadSafeComponentExtensionPoint<? extends ModelItemType> _modelItemTypeExtensionPoint;
046    
047    /**
048     * Creates a model item parser.
049     * @param modelItemTypeExtensionPoint the extension point to use to get available model items types
050     */
051    public AbstractModelItemParser(AbstractThreadSafeComponentExtensionPoint<? extends ModelItemType> modelItemTypeExtensionPoint)
052    {
053        _modelItemTypeExtensionPoint = modelItemTypeExtensionPoint;
054    }
055    
056    /**
057     * Parses an element definition from a XML configuration.
058     * @param <T> type of the parsed item
059     * @param serviceManager the service manager
060     * @param pluginName the plugin name declaring this item.
061     * @param itemConfig the XML configuration of the model item.
062     * @param model the model which defines the model item
063     * @param parent the parent of the model item to create. Can be null if the model item to parse has no parent
064     * @return the parsed model item.
065     * @throws ConfigurationException if the configuration is not valid.
066     */
067    @SuppressWarnings("unchecked")
068    public <T extends ModelItem> T parse(ServiceManager serviceManager, String pluginName, Configuration itemConfig, Model model, ModelItemGroup parent) throws ConfigurationException
069    {
070        return (T) parse(serviceManager, pluginName, "plugin." + pluginName, itemConfig, model, parent);
071    }
072    
073    /**
074     * Parses an element definition from a XML configuration.
075     * @param <T> type of the parsed item
076     * @param serviceManager the service manager
077     * @param pluginName the plugin name declaring this item.
078     * @param catalog the catalog
079     * @param itemConfig the XML configuration of the model item.
080     * @param model the model which defines the model item
081     * @param parent the parent of the model item to create. Can be null if the model item to parse has no parent
082     * @return the parsed model item.
083     * @throws ConfigurationException if the configuration is not valid.
084     */
085    @SuppressWarnings("unchecked")
086    public <T extends ModelItem> T parse(ServiceManager serviceManager, String pluginName, String catalog, Configuration itemConfig, Model model, ModelItemGroup parent) throws ConfigurationException
087    {
088        ModelItem modelItem = _createModelItem(itemConfig);
089        
090        modelItem.setModel(model);
091        if (parent != null)
092        {
093            parent.addChild(modelItem);
094        }
095        
096        modelItem.setName(_parseName(itemConfig));
097        modelItem.setPluginName(pluginName);
098        modelItem.setLabel(_parseI18nizableText(itemConfig, catalog, "label"));
099        modelItem.setDescription(_parseI18nizableText(itemConfig, catalog, "description"));
100        modelItem.setType(_parseType(itemConfig));
101        
102        modelItem.setWidget(_parseWidget(itemConfig));
103        modelItem.setWidgetParameters(_parseWidgetParameters(itemConfig, pluginName));
104        modelItem.setDisableConditions(_parseDisableConditions(itemConfig));
105        
106        return (T) modelItem;
107    }
108    
109    /**
110     * Create the model item to populate it.
111     * @param itemConfig the model item configuration to use.
112     * @return the item instantiated.
113     * @throws ConfigurationException if the configuration is not valid.
114     */
115    protected abstract ModelItem _createModelItem(Configuration itemConfig) throws ConfigurationException;
116
117    /**
118     * Parses the name of the model item.
119     * @param itemConfig the model item configuration to use.
120     * @return the name of the model item.
121     * @throws ConfigurationException if the configuration is not valid.
122     */
123    protected String _parseName(Configuration itemConfig) throws ConfigurationException
124    {
125        String itemName = itemConfig.getAttribute(_getNameConfigurationAttribute());
126        
127        if (!itemName.matches("^[a-zA-Z]((?!__)[a-zA-Z0-9_\\.-])*$"))
128        {
129            throw new ConfigurationException("Invalid model item name: " + itemName + ". The item name must start with a letter and must contain only letters, digits, underscore, dot or dash characters.", itemConfig);
130        }
131        
132        return itemName;
133    }
134    
135    /**
136     * Retrieves the name of the configuration attribute that contains the name of the model item
137     * @return the name of the configuration attribute that contains the name of the model item
138     */
139    protected String _getNameConfigurationAttribute()
140    {
141        return "name";
142    }
143    
144    /**
145     * Parses a mandatory i18n text configuration, throwing an exception if empty.
146     * @param config the configuration to use.
147     * @param catalog the catalog
148     * @param name the child name.
149     * @return the i18n text.
150     * @throws ConfigurationException if the configuration is not valid.
151     */
152    protected I18nizableText _parseI18nizableText(Configuration config, String catalog, String name) throws ConfigurationException
153    {
154        return I18nizableText.parseI18nizableText(config.getChild(name), catalog);
155    }
156    
157    /**
158     * Parse an optional i18n text configuration, with a default i18n text value.
159     * @param config the configuration to use.
160     * @param catalog the catalog
161     * @param name the child name.
162     * @param defaultValueCatalog the catalog of the default i18n text value
163     * @param defaultValue the default i18n text value.
164     * @return the i18n text.
165     */
166    protected I18nizableText _parseI18nizableText(Configuration config, String catalog, String name, String defaultValueCatalog, String defaultValue)
167    {
168        return I18nizableText.parseI18nizableText(config.getChild(name), catalog, new I18nizableText(defaultValueCatalog, defaultValue));
169    }
170    
171    /**
172     * Parses the type.
173     * @param config the model item configuration to use.
174     * @return the type.
175     * @throws ConfigurationException if the configuration is not valid.
176     */
177    protected ModelItemType _parseType(Configuration config) throws ConfigurationException
178    {
179        String typeId = config.getAttribute("type");
180        
181        if (!_modelItemTypeExtensionPoint.hasExtension(typeId))
182        {
183            String modelItemName = _parseName(config);
184            String availableTypes = StringUtils.join(_modelItemTypeExtensionPoint.getExtensionsIds(), ", ");
185            UnknownTypeException ute = new UnknownTypeException("The type '" + typeId + "' is not available for the extension point '" + _modelItemTypeExtensionPoint + "'. Available types are: '" + availableTypes + "'.");
186            throw new ConfigurationException("Unable to find the type '" + typeId + "' defined on the item '" + modelItemName + "'.", ute);
187        }
188        
189        return _modelItemTypeExtensionPoint.getExtension(typeId);
190    }
191    
192    /**
193     * Parses the disable condition.
194     * @param definitionConfiguration the configuration of the disable condition
195     * @return result the parsed disable condition to be converted in JSON
196     * @throws ConfigurationException if an error occurred
197     */
198    protected DisableConditions _parseDisableConditions(Configuration definitionConfiguration) throws ConfigurationException
199    {
200        Configuration disableConditionConfiguration = definitionConfiguration.getChild("disable-conditions", false);
201        DisableConditions conditions = null;
202        
203        if (disableConditionConfiguration != null)
204        {
205            conditions = new DisableConditions();
206
207            Configuration[] conditionsConfiguration = disableConditionConfiguration.getChildren();
208            for (Configuration conditionConfiguration : conditionsConfiguration)
209            {
210                String tagName = conditionConfiguration.getName();
211                
212                // Recursive case
213                if (tagName.equals("conditions"))
214                {
215                    conditions.getSubConditions().add(_parseDisableConditions(conditionConfiguration));
216                }
217                else if (tagName.equals("condition"))
218                {
219                    String id = conditionConfiguration.getAttribute("id");
220                    DisableCondition.OPERATOR operator = DisableCondition.OPERATOR.valueOf(conditionConfiguration.getAttribute("operator", "eq").toUpperCase());
221                    String value = conditionConfiguration.getValue("");
222                    
223                    
224                    DisableCondition condition = new DisableCondition(id, operator, value);
225                    conditions.getConditions().add(condition);
226                }
227            }
228            
229            conditions.setAssociation(DisableConditions.ASSOCIATION_TYPE.valueOf(disableConditionConfiguration.getAttribute("type", "and").toUpperCase()));
230        }
231        
232        return conditions;
233    }    
234
235    /**
236     * Parses the widget.
237     * @param definitionConfig the element definition configuration to use.
238     * @return the widget or <code>null</code> if none defined.
239     * @throws ConfigurationException if the configuration is not valid.
240     */
241    protected String _parseWidget(Configuration definitionConfig) throws ConfigurationException
242    {
243        return definitionConfig.getChild("widget").getValue(null);
244    }
245    
246    /**
247     * Parses the widget's parameters
248     * @param definitionConfig the parameter ele;ent definition to use.
249     * @param pluginName the current plugin name.
250     * @return the widget's parameters in a Map
251     * @throws ConfigurationException if the configuration is not valid.
252     */
253    protected Map<String, I18nizableText> _parseWidgetParameters(Configuration definitionConfig, String pluginName) throws ConfigurationException
254    {
255        Map<String, I18nizableText> widgetParams = new HashMap<>();
256        
257        Configuration widgetParamsConfig = definitionConfig.getChild("widget-params", false);
258        if (widgetParamsConfig != null)
259        {
260            Map<String, Object> parsedParams = ConfigurationHelper.parsePluginParameters(widgetParamsConfig, pluginName, __LOGGER);
261            
262            for (Entry<String, Object> param : parsedParams.entrySet())
263            {
264                String paramName = param.getKey();
265                Object value = param.getValue();
266                if (value instanceof I18nizableText)
267                {
268                    widgetParams.put(paramName, (I18nizableText) value);
269                }
270                else if (value instanceof String)
271                {
272                    widgetParams.put(paramName, new I18nizableText((String) value));
273                }
274                else
275                {
276                    __LOGGER.warn("Widget parameter '{}' at location {} is of type [{}] which is not supported. It will be ignored.", paramName, definitionConfig.getLocation(), value.getClass());
277                }
278            }
279        }
280        
281        return widgetParams;
282    }
283}