001/*
002 *  Copyright 2015 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.plugins.core.ui.util;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.configuration.Configuration;
025import org.apache.avalon.framework.configuration.ConfigurationException;
026import org.apache.commons.lang3.StringUtils;
027import org.slf4j.Logger;
028
029import org.ametys.core.ui.ClientSideElement.ScriptFile;
030import org.ametys.runtime.config.Config;
031import org.ametys.runtime.i18n.I18nizableText;
032
033/**
034 * Helper class providing methods to deal with common {@link Configuration} tasks.
035 */
036public final class ConfigurationHelper
037{
038    
039    private ConfigurationHelper()
040    {
041        // Helper class, never to be instantiated.
042    }
043    
044    /**
045     * Parse a plugin resource list configuration.
046     * @param configuration The plugin resource list configuration.
047     * @param defaultPluginName The default plugin name to use for the resources. 
048     * @param logger The logger.
049     * @return The list of complete URLs of files to import.
050     * @throws ConfigurationException If an error occurs
051     */
052    public static List<ScriptFile> parsePluginResourceList(Configuration configuration, String defaultPluginName, Logger logger) throws ConfigurationException
053    {
054        List<ScriptFile> resourceFiles = new ArrayList<>();
055        String listDefaultPlugin = configuration.getAttribute("plugin", defaultPluginName);
056        for (Configuration fileConfiguration : configuration.getChildren("file"))
057        {
058            ScriptFile scriptFile = _getPluginResourceValue(fileConfiguration, listDefaultPlugin, logger);
059            
060            resourceFiles.add(scriptFile);
061        }
062        return resourceFiles;
063    }
064
065    private static ScriptFile _getPluginResourceValue(Configuration fileConfiguration, String listDefaultPlugin, Logger logger) throws ConfigurationException
066    {
067        ScriptFile scriptFile;
068
069        boolean langAware = fileConfiguration.getAttributeAsBoolean("lang", false);
070        if (langAware)
071        {
072            String defaultLang = fileConfiguration.getAttribute("defaultLang", null);
073            
074            Map<String, String> langPaths = new HashMap<>();
075            for (Configuration langConfiguration : fileConfiguration.getChildren("lang"))
076            {
077                String code = langConfiguration.getAttribute("code", null);
078                if (StringUtils.isBlank(code))
079                {
080                    throw new ConfigurationException("Code attribute is mandatory for lang tag", langConfiguration);
081                }
082                
083                String path = _getPluginResourceValue(langConfiguration, listDefaultPlugin, langConfiguration.getValue(), logger);
084                langPaths.put(code, path);
085            }
086            
087            scriptFile = new ScriptFile(langPaths, defaultLang);
088        }
089        else
090        {
091            String rtl = fileConfiguration.getAttribute("rtl", "all");
092            String path = _getPluginResourceValue(fileConfiguration, listDefaultPlugin, fileConfiguration.getValue(), logger);
093            
094            scriptFile = new ScriptFile(rtl, path);
095        }
096        
097        return scriptFile;
098    }
099    
100    /**
101     * Parse a mandatory plugin resource configuration.
102     * @param configuration The plugin resource configuration.
103     * @param defaultPluginName The default plugin name to use for the resources. 
104     * @param logger The logger.
105     * @return The plugin resource full URL.
106     * @throws ConfigurationException If an error occurs
107     */
108    public static String parsePluginResource(Configuration configuration, String defaultPluginName, Logger logger) throws ConfigurationException
109    {
110        String url = configuration.getValue();
111        return _getPluginResourceValue(configuration, defaultPluginName, url, logger);
112    }
113    
114    /**
115     * Parse an optional plugin resource configuration.
116     * @param configuration The plugin resource configuration.
117     * @param defaultPluginName The default plugin name to use for the resources. 
118     * @param defaultValue The default value
119     * @param logger The logger.
120     * @return The plugin resource full URL.
121     */
122    public static String parsePluginResource(Configuration configuration, String defaultPluginName, String defaultValue, Logger logger)
123    {
124        String url = configuration.getValue(defaultValue);
125        return _getPluginResourceValue(configuration, defaultPluginName, url, logger);
126    }
127    
128    /**
129     * Get a plugin resource configuration value.
130     * @param configuration The plugin resource configuration.
131     * @param defaultPluginName The default plugin name to use for the resources. 
132     * @param value The value to parse. 
133     * @param logger The logger.
134     * @return The plugin resource full URL.
135     */
136    private static String _getPluginResourceValue(Configuration configuration, String defaultPluginName, String value, Logger logger)
137    {
138        String pluginName = configuration.getAttribute("plugin", defaultPluginName);
139        
140        String fullUrl = "/plugins/" + pluginName + "/resources/" + value;
141        
142        if (logger.isDebugEnabled())
143        {
144            logger.debug("Importing file '" + fullUrl + "'");
145        }
146        
147        return fullUrl;
148    }
149    
150    /**
151     * Parse a plugin files list configuration and return the list of URIs.
152     * @param configuration The plugin files list configuration.
153     * @param defaultPluginName The default plugin name to use for the files. 
154     * @param logger The logger.
155     * @return The list of complete URIs of files to import.
156     * @throws ConfigurationException If an error occurs
157     */
158    public static List<String> parsePluginResourceUri(Configuration configuration, String defaultPluginName, Logger logger) throws ConfigurationException
159    {
160        List<String> fileURIs = new ArrayList<>();
161        String listDefaultPlugin = configuration.getAttribute("plugin", defaultPluginName);
162        for (Configuration fileConfiguration : configuration.getChildren("file"))
163        {
164            String fileURI = _getPluginResourceUri(fileConfiguration, listDefaultPlugin, logger);
165            
166            fileURIs.add(fileURI);
167        }
168        return fileURIs;
169    }
170    
171    /**
172     * Get a plugin resource configuration value.
173     * @param configuration The plugin resource configuration.
174     * @param defaultPluginName The default plugin name to use for the resources. 
175     * @param logger The logger.
176     * @return The plugin resource full URL.
177     * @throws ConfigurationException If an error occurs
178     */
179    private static String _getPluginResourceUri(Configuration configuration, String defaultPluginName, Logger logger) throws ConfigurationException
180    {
181        String pluginName = configuration.getAttribute("plugin", defaultPluginName);
182        
183        String value = configuration.getValue();
184        
185        String fullUrl = "plugin:" + pluginName + "://" + value;
186        
187        if (logger.isDebugEnabled())
188        {
189            logger.debug("Importing file uri '" + fullUrl + "'");
190        }
191        
192        return fullUrl;
193    }
194    
195    /**
196     * Parse a mandatory configuration parameter {@link Configuration}.
197     * @param configuration The {@link Configuration} to parse.
198     * @return the configuration parameter value.
199     * @throws ConfigurationException if an error occurs.
200     */
201    public static String parseConfigParameter(Configuration configuration) throws ConfigurationException
202    {
203        return _getConfigParameterValue(configuration.getValue());
204    }
205    
206    /**
207     * Parse an optional configuration parameter {@link Configuration}.
208     * @param configuration The {@link Configuration} to parse.
209     * @param defaultValue The default value.
210     * @return the configuration parameter value.
211     */
212    public static String parseConfigParameter(Configuration configuration, String defaultValue)
213    {
214        return _getConfigParameterValue(configuration.getValue(defaultValue));
215    }
216    
217    /**
218     * Get a configuration parameter value.
219     * @param value The key to get in configuration
220     * @return the configuration parameter value.
221     */
222    private static String _getConfigParameterValue(String value)
223    {
224        return Config.getInstance().getValue(value);
225    }
226    
227    /**
228     * Parse a structured Configuration to an Object.
229     * The returned Object can be either a String or a Map&lt;String, Object&gt;.
230     * The map's values are either a String, a Map, or a List&lt;String&gt;.
231     * @param configuration The structured Configuration to parse.
232     * @return The parsed Object.
233     */
234    public static Object parseObject(Configuration configuration)
235    {
236        return parseObject(configuration, null);
237    }
238    
239    /**
240     * Parse a structured Configuration to an Object.
241     * The returned Object can be either a String or a Map&lt;String, Object&gt;.
242     * The map's values are either a String, a Map, or a List&lt;String&gt;.
243     * @param configuration The structured Configuration to parse.
244     * @param defaultValue The value to use when an empty tag is found (at any level in the tree).
245     * @return The parsed Object.
246     */
247    @SuppressWarnings("unchecked")
248    public static Object parseObject(Configuration configuration, Object defaultValue)
249    {
250        String value = configuration.getValue(null);
251        Configuration[] children = configuration.getChildren();
252        
253        if (value != null)
254        {
255            // Mixed content cannot be found in a Configuration: if it has a value,
256            // it won't have any child.
257            return value;
258        }
259        else if (children.length > 0)
260        {
261            Map<String, Object> result = new LinkedHashMap<>();
262            for (Configuration subConf : children)
263            {
264                String name = subConf.getName();
265                Object subValue = parseObject(subConf, defaultValue);
266                boolean multiple = configuration.getChildren(name).length > 1;
267                
268                if (multiple)
269                {
270                    // More than one value with the same name: make it a list and append the current value.
271                    List<Object> values = null;
272                    if (result.containsKey(name))
273                    {
274                        values = (List<Object>) result.get(name);
275                    }
276                    else
277                    {
278                        values = new ArrayList<>();
279                        result.put(name, values);
280                    }
281                    values.add(subValue);
282                }
283                else
284                {
285                    // Only one value with this name: add it as a single value.
286                    result.put(name, subValue);
287                }
288            }
289            return result;
290        }
291        else
292        {
293            return defaultValue;
294        }
295    }
296    
297    /**
298     * Parse parameters recursively.
299     * @param configuration the parameters configuration.
300     * @param defaultPluginName The default plugin name.
301     * @param logger The logger.
302     * @return parameters in a Map
303     * @throws ConfigurationException If the configuration is incorrect.
304     */
305    public static Map<String, Object> parsePluginParameters(Configuration configuration, String defaultPluginName, Logger logger) throws ConfigurationException
306    {
307        Map<String, Object> parameters = new LinkedHashMap<>();
308        
309        for (Configuration paramConfiguration : configuration.getChildren())
310        {
311            String name;
312            if (paramConfiguration.getName().equals("param"))
313            {
314                name = paramConfiguration.getAttribute("name");
315            }
316            else
317            {
318                name = paramConfiguration.getName();
319            }
320            String value = paramConfiguration.getValue("");
321            
322            if (logger.isDebugEnabled())
323            {
324                logger.debug("Configured with parameter '" + name + "' : '" + value + "'");
325            }
326            
327            if (isI18n(paramConfiguration))
328            {
329                addParameter(parameters, name, I18nizableText.getI18nizableTextValue(paramConfiguration, "plugin." + defaultPluginName, value));
330            }
331            else if (isResourceFile(paramConfiguration))
332            {
333                addParameter(parameters, name, _getPluginResourceValue(paramConfiguration, defaultPluginName, value, logger));
334            }
335            else if (isConfigParameter(paramConfiguration))
336            {
337                addParameter(parameters, name, _getConfigParameterValue(value));
338            }
339            else if (paramConfiguration.getChildren().length != 0)
340            {
341                addParameter(parameters, name, parsePluginParameters(paramConfiguration, defaultPluginName, logger));
342            }
343            else 
344            {
345                addParameter(parameters, name, value);
346            }
347        }
348        
349        return parameters;
350    }
351    
352    /**
353     * Clone existing parameters
354     * @param parameters The existing parameters
355     * @return The cloned
356     */
357    public static Map<String, Object> clonePluginParameters(Map<String, Object> parameters)
358    {
359        Map<String, Object> clonedParameters = new LinkedHashMap<>();
360        
361        for (String name: parameters.keySet())
362        {
363            Object value = parameters.get(name);
364            
365            clonedParameters.put(name, _clonePluginParameter(value));
366        }
367        
368        return clonedParameters;
369    }
370    
371    @SuppressWarnings("unchecked")
372    private static Object _clonePluginParameter(Object value)
373    {
374        if (value instanceof I18nizableText 
375                || value instanceof String
376                // after this point, types cannot be created by static configuration, but are "JSONUtils compatible" and may be used by dynamic ClientSideElements
377                || value instanceof Boolean
378                || value instanceof Integer
379                || value instanceof Long
380                || value instanceof Double)
381        {
382            return value;
383        }
384        else if (value instanceof LinkedHashMap)
385        {
386            return clonePluginParameters((Map<String, Object>) value);
387        }
388        else if (value instanceof ArrayList)
389        {
390            List list = new ArrayList();
391            for (Object v : (List) value)
392            {
393                list.add(_clonePluginParameter(v));
394            }
395            return list; 
396        }
397        else
398        {
399            throw new IllegalArgumentException("Cannot clone a parameter of type '" + value.getClass().getName() + "'");
400        }
401    }
402    
403    @SuppressWarnings("unchecked")
404    private static void addParameter(Map<String, Object> parameters, String name, Object newValue)
405    {
406        if (parameters.containsKey(name))
407        {
408            Object values = parameters.get(name);
409            if (values instanceof List)
410            {
411                ((List<Object>) values).add(newValue);
412            }
413            else
414            {
415                List list = new ArrayList<>();
416                list.add(values);
417                list.add(newValue);
418                parameters.put(name, list);
419            }
420        }
421        else
422        {
423            parameters.put(name, newValue);
424        }
425    }
426    
427    private static boolean isResourceFile(Configuration config)
428    {
429        return config.getAttributeAsBoolean("file", false) || config.getAttribute("type", "").equals("file");
430    }
431    
432    private static boolean isConfigParameter(Configuration config)
433    {
434        return config.getAttributeAsBoolean("config", false) || config.getAttribute("type", "").equals("config");
435    }
436    
437    private static boolean isI18n(Configuration config)
438    {
439        return config.getAttributeAsBoolean("i18n", false) || config.getAttribute("type", "").equals("i18n");
440    }
441}