001/*
002 *  Copyright 2024 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.configuration.Configuration;
025import org.apache.avalon.framework.configuration.ConfigurationException;
026import org.apache.commons.lang3.StringUtils;
027import org.apache.commons.lang3.tuple.ImmutablePair;
028import org.apache.commons.lang3.tuple.Pair;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import org.ametys.plugins.core.ui.util.ConfigurationHelper;
033import org.ametys.runtime.config.ConfigManager;
034import org.ametys.runtime.i18n.I18nizableText;
035import org.ametys.runtime.model.type.ElementType;
036
037/**
038 * Helper for all {@link ModelItem} parsers
039 */
040public final class ItemParserHelper
041{
042    private static final Logger __LOGGER = LoggerFactory.getLogger(ModelHelper.class);
043    
044    private ItemParserHelper()
045    {
046        // Empty constructor
047    }
048    
049    /**
050     * Parses the name of the model item
051     * @param itemConfig the model item's configuration to parse
052     * @param nameConfigurationAttribute the name of the attribute containing the item's name
053     * @return the name of the model item
054     * @throws ConfigurationException if an error occurs
055     */
056    public static String parseName(Configuration itemConfig, String nameConfigurationAttribute) throws ConfigurationException
057    {
058        return parseName(itemConfig, nameConfigurationAttribute, true);
059    }
060    
061    /**
062     * Parses the name of the model item
063     * @param itemConfig the model item's configuration to parse
064     * @param nameConfigurationAttribute the name of the attribute containing the item's name
065     * @param acceptDots <code>true</code> if the name to parse can contain dots, <code>false</code> otherwise
066     * @return the name of the model item
067     * @throws ConfigurationException if an error occurs
068     */
069    public static String parseName(Configuration itemConfig, String nameConfigurationAttribute, boolean acceptDots) throws ConfigurationException
070    {
071        String itemName = itemConfig.getAttribute(nameConfigurationAttribute);
072        
073        String regex = acceptDots
074                ? "^[a-zA-Z]((?!__)[a-zA-Z0-9_\\.-])*$"
075                : "^[a-zA-Z]((?!__)[a-zA-Z0-9_-])*$";
076        if (!itemName.matches(regex))
077        {
078            StringBuilder message = new StringBuilder()
079                    .append("Invalid model item name: ").append(itemName).append(".")
080                    .append(" The item name must start with a letter and must contain only letters, digits, underscores").append(acceptDots ? ", dots " : " ").append("or dashes.");
081            throw new ConfigurationException(message.toString(), itemConfig);
082        }
083        
084        return itemName;
085    }
086    
087    /**
088     * Parses the multiple attribute.
089     * @param itemConfig the item configuration to use.
090     * @return <code>true</code> if the item is multiple, <code>false</code> otherwise.
091     * @throws ConfigurationException if the configuration is not valid.
092     */
093    public static Boolean parseMultiple(Configuration itemConfig) throws ConfigurationException
094    {
095        return itemConfig.getAttributeAsBoolean("multiple", false);
096    }
097    
098    /**
099     * Parses the default values.
100     * @param itemConfig the item configuration.
101     * @param definition the element definition.
102     * @param defaultValueParser The {@link DefaultValueParser} to use to parse single default values
103     * @return the default values and their types or <code>null</code> if none default value is defined.
104     * @throws ConfigurationException if the configuration is not valid.
105     */
106    public static List<Pair<String, Object>> parseDefaultValues(Configuration itemConfig, ElementDefinition definition, DefaultValueParser defaultValueParser) throws ConfigurationException
107    {
108        Configuration[] defaultValueConfigs = itemConfig.getChildren("default-value");
109        if (defaultValueConfigs.length > 0)
110        {
111            List<Pair<String, Object>> defaultValues = new ArrayList<>();
112            
113            for (Configuration defaultValueConfig : defaultValueConfigs)
114            {
115                String defaultValueType = defaultValueConfig.getAttribute("type", null);
116                Object defaultValue = defaultValueParser.parseDefaultValue(defaultValueConfig, definition, defaultValueType);
117                defaultValues.add(new ImmutablePair<>(defaultValueType, defaultValue));
118            }
119            
120            return defaultValues;
121        }
122        else
123        {
124            return null;
125        }
126    }
127    
128    /**
129     * Parses the default value.
130     * @param defaultValueConfig the default value configuration.
131     * @param definition the element definition.
132     * @param defaultValueType the type of the default value
133     * @return the default value or <code>null</code> if none default value is defined.
134     * @throws ConfigurationException if the configuration is not valid.
135     */
136    public static Object parseDefaultValue(Configuration defaultValueConfig, ElementDefinition definition, String defaultValueType) throws ConfigurationException
137    {
138        ElementType type = definition.getType();
139        if (defaultValueType != null)
140        {
141            if (ElementDefinition.CONFIG_DEFAULT_VALUE_TYPE.equals(defaultValueType))
142            {
143                String configParamName = defaultValueConfig.getValue();
144                if (ConfigManager.getInstance().hasModelItem(configParamName))
145                {
146                    ModelItem configParamDefinition = ConfigManager.getInstance().getModelItem(configParamName);
147                    String configParamTypeId = configParamDefinition.getType().getId();
148                    if (configParamTypeId.equals(type.getId()))
149                    {
150                        return configParamName;
151                    }
152                    else
153                    {
154                        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);
155                    }
156                }
157                else
158                {
159                    throw new ConfigurationException("The configuration parameter '" + configParamName + "' does not exist, it cannot be used as default value for item '" + definition.getPath() + " (" + type.getId() + ")'.", defaultValueConfig);
160                }
161            }
162            else
163            {
164                throw new ConfigurationException("The type '" + defaultValueType + "' is not available for the default value of item '" + definition.getPath() + " (" + type.getId() + ")'.", defaultValueConfig);
165            }
166        }
167        else
168        {
169            return type.parseConfiguration(defaultValueConfig);
170        }
171    }
172    
173    /**
174     * Parses the widget.
175     * @param itemConfig the item configuration to use.
176     * @return the widget or <code>null</code> if none defined.
177     * @throws ConfigurationException if the configuration is not valid.
178     */
179    public static String parseWidget(Configuration itemConfig) throws ConfigurationException
180    {
181        return itemConfig.getChild("widget").getValue(null);
182    }
183    
184    /**
185     * Parses the widget's parameters
186     * @param itemConfig the item configuration to use.
187     * @param pluginName the current plugin name.
188     * @return the widget's parameters in a Map
189     * @throws ConfigurationException if the configuration is not valid.
190     */
191    public static Map<String, I18nizableText> parseWidgetParameters(Configuration itemConfig, String pluginName) throws ConfigurationException
192    {
193        Map<String, I18nizableText> widgetParams = new HashMap<>();
194        
195        Configuration widgetParamsConfig = itemConfig.getChild("widget-params", false);
196        if (widgetParamsConfig != null)
197        {
198            Map<String, Object> parsedParams = ConfigurationHelper.parsePluginParameters(widgetParamsConfig, pluginName, __LOGGER);
199            
200            for (Entry<String, Object> param : parsedParams.entrySet())
201            {
202                String paramName = param.getKey();
203                Object value = param.getValue();
204                if (value instanceof I18nizableText)
205                {
206                    widgetParams.put(paramName, (I18nizableText) value);
207                }
208                else if (value instanceof String)
209                {
210                    widgetParams.put(paramName, new I18nizableText((String) value));
211                }
212                else
213                {
214                    __LOGGER.warn("Widget parameter '{}' at location {} is of type [{}] which is not supported. It will be ignored.", paramName, itemConfig.getLocation(), value.getClass());
215                }
216            }
217        }
218        
219        return widgetParams;
220    }
221    
222    /**
223     * Parse an i18n text.
224     * @param configurationAndPluginName the configuration to use.
225     * @param name the child name.
226     * @return the i18n text.
227     * @throws ConfigurationException if the configuration is not valid.
228     */
229    public static I18nizableText parseI18nizableText(ConfigurationAndPluginName configurationAndPluginName, String name) throws ConfigurationException
230    {
231        return parseI18nizableText(configurationAndPluginName, name, StringUtils.EMPTY);
232    }
233    
234    /**
235     * Parse an i18n text.
236     * @param configurationAndPluginName the configuration to use.
237     * @param name the child name.
238     * @param defaultValue the default value if no present
239     * @return the i18n text.
240     * @throws ConfigurationException if the configuration is not valid.
241     */
242    public static I18nizableText parseI18nizableText(ConfigurationAndPluginName configurationAndPluginName, String name, String defaultValue) throws ConfigurationException
243    {
244        return I18nizableText.parseI18nizableText(configurationAndPluginName.configuration().getChild(name), "plugin." + configurationAndPluginName.pluginName(), defaultValue);
245    }
246    
247    /**
248     * Parse an i18n text.
249     * @param configurationAndPluginName the configuration to use.
250     * @param name the child name.
251     * @param defaultValue the default value if no present
252     * @return the i18n text.
253     * @throws ConfigurationException if the configuration is not valid.
254     */
255    public static I18nizableText parseI18nizableText(ConfigurationAndPluginName configurationAndPluginName, String name, I18nizableText defaultValue) throws ConfigurationException
256    {
257        return I18nizableText.parseI18nizableText(configurationAndPluginName.configuration().getChild(name), "plugin." + configurationAndPluginName.pluginName(), defaultValue);
258    }
259    
260    /**
261     * Stores a configuration and the name of the plugin in which is declared the configuration
262     * @param configuration the configuration itself
263     * @param pluginName the name of the plugin in which is declared the configuration
264     */
265    public record ConfigurationAndPluginName(Configuration configuration, String pluginName) { /* empty */ }
266    
267    /**
268     * Default value parser
269     */
270    @FunctionalInterface
271    public interface DefaultValueParser
272    {
273        /**
274         * Parses the default value.
275         * @param defaultValueConfig the default value configuration.
276         * @param definition the element definition.
277         * @param defaultValueType the type of the default value
278         * @return the default value or <code>null</code> if none default value is defined.
279         * @throws ConfigurationException if the configuration is not valid.
280         */
281        public Object parseDefaultValue(Configuration defaultValueConfig, ElementDefinition definition, String defaultValueType) throws ConfigurationException;
282    }
283}