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.config;
017
018import java.io.File;
019import java.util.HashMap;
020import java.util.Map;
021import java.util.Map.Entry;
022import java.util.Set;
023
024import org.apache.avalon.framework.configuration.Configuration;
025import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import org.ametys.runtime.model.DefinitionAndValue;
030import org.ametys.runtime.model.ElementDefinition;
031import org.ametys.runtime.model.Model;
032import org.ametys.runtime.model.ModelItem;
033import org.ametys.runtime.model.type.DataContext;
034import org.ametys.runtime.model.type.ElementType;
035import org.ametys.runtime.model.type.xml.XMLElementType;
036
037/**
038 * Bean to read / Write config file
039 */
040public final class Config
041{
042    // Logger for traces
043    private static Logger __logger = LoggerFactory.getLogger(Config.class);
044    
045    // shared instance for optimization
046    private static Config __config;
047    
048    // Initialization status
049    private static boolean __initialized;
050    
051    // config file
052    private static String __filename;
053    
054    // true if the config file does exist
055    private static boolean __fileExists;
056
057    // The last modification date
058    private static long __lastModified = -1;
059    
060    // The configuration model
061    private static Model __model;
062    
063    // Typed value (filled after read)
064    private Map<String, DefinitionAndValue> _definitionAndValues;
065    
066    private Config()
067    {
068    }
069
070    /**
071     * Get the instance of Config using the config file.
072     * @return An instance of Config containing config file values and definition, or null if config file cannot be read.
073     */
074    public static Config getInstance()
075    {
076        if (!__initialized)
077        {
078            return null;
079        }
080        
081        if (__config == null)
082        {
083            try
084            {
085                __config = new Config();
086                __config._load();
087            }
088            catch (Exception e)
089            {
090                __logger.warn("Exception creating Config, it won't be accessible.", e);
091                return null;
092            }
093        }
094        else
095        {
096            __config._reloadIfNeeded();
097        }
098        
099        return __config;
100    }
101    
102    /**
103     * Sets the configuration model
104     * @param model the model to set
105     */
106    public static void setModel(Model model)
107    {
108        __model = model;
109    }
110    
111    private void _load() throws Exception
112    {
113        __logger.info("Loading configuration values from file {}.", __filename);
114        _definitionAndValues = __read();
115    }
116    
117    private void _reloadIfNeeded()
118    {
119        if (__fileExists && new File(__filename).exists() && __lastModified < new File(__filename).lastModified())
120        {
121            try
122            {
123                __logger.info("The config file has changed. Let's reload."); 
124                _load();
125            }
126            catch (Exception e)
127            {
128                // __lastModified was changed, so we will not fail several times
129                // _values was not modified
130                // __fileExists is still true
131                __logger.error("The config file '" + __filename + "' was modified but could not be reloaded due to an exception", e);
132            }
133        }
134    }
135    
136    /**
137     * Read configuration file
138     * @return the configuration definition and values
139     * @throws Exception if a problem occurs reading values
140     */
141    static Map<String, DefinitionAndValue> __read() throws Exception
142    {
143        Map<String, DefinitionAndValue> definitionAndValues = new HashMap<>();
144
145        File configFile = new File(__filename);
146        __fileExists = configFile.exists();
147
148        if (__fileExists)
149        {
150            __lastModified  = configFile.lastModified();
151
152            Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(configFile);
153            for (Configuration parameter : configuration.getChildren())
154            {
155                String parameterName = parameter.getName();
156                if (__model.hasModelItem(parameterName))
157                {
158                    ModelItem modelItem = __model.getModelItem(parameterName);
159    
160                    if (modelItem instanceof ElementDefinition)
161                    {
162                        ElementDefinition definition = (ElementDefinition) modelItem;
163                        ElementType type = definition.getType();
164                        
165                        if (type instanceof XMLElementType)
166                        {
167                            Object value = ((XMLElementType) type).read(configuration, parameterName);
168                            DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, definition, value);
169                            definitionAndValues.put(parameterName, definitionAndValue);
170                        }
171                    }
172                }
173                else
174                {
175                    __logger.warn("The parameter {} is not defined in configuration. This parameter is ignored.", parameterName);
176                }
177            }
178        }
179        else
180        {
181            __lastModified = -1;
182        }
183
184        return definitionAndValues;
185    }
186    
187    /**
188     * Set the initialization status of the configuration
189     * @param initialized the initialization status of the configuration
190     */
191    public static void setInitialized(boolean initialized)
192    {
193        __initialized = initialized;
194    }
195
196    /**
197     * Set the configuration filename
198     * @param filename Name with path of the configuration file
199     */
200    public static void setFilename(String filename)
201    {
202        __filename = filename;
203        __fileExists = new File(__filename).exists();
204    }
205    
206    /**
207     * Returns true if the configuration filename exists.
208     * @return true if the configuration filename exists.
209     */
210    public static boolean fileExists()
211    {
212        return __fileExists;
213    }
214    
215    /**
216     * Retrieves all configuration parameter values
217     * @return the configuration values
218     */
219    public Map<String, Object> getValues()
220    {
221        return __extractValues(_definitionAndValues);
222    }
223    
224    static Map<String, Object> __extractValues(Map<String, DefinitionAndValue> definitionAndValues)
225    {
226        return DefinitionAndValue.extractValues(definitionAndValues);
227    }
228    
229    /**
230     * Retrieves all configuration parameter values as JSON objects
231     * @return the configuration values as JSON objects
232     */
233    public static Map<String, Object> getValuesAsJSONForClient()
234    {
235        Map<String, Object> jsonValues = new HashMap<>();
236        
237        Set<Entry<String, DefinitionAndValue>> values;
238        
239        try
240        {
241            values = __read().entrySet();
242        }
243        catch (Exception e)
244        {
245            __logger.warn("Config values are unreadable. Using default values", e);
246            return jsonValues;
247        }
248        
249        for (Map.Entry<String, DefinitionAndValue> entry : values)
250        {
251            DefinitionAndValue definitionAndValue = entry.getValue();
252            
253            Object value = definitionAndValue.getValue();
254            ElementDefinition definition = (ElementDefinition) definitionAndValue.getDefinition();
255            
256            ElementType<Object> type = definition.getType();
257            Object jsonValue = type.valueToJSONForClient(value, DataContext.newInstance());
258            
259            jsonValues.put(entry.getKey(), jsonValue);
260        }
261        
262        return jsonValues;
263        
264    }
265    
266    /**
267     * Retrieves the typed parameter value
268     * @param <T> type of the value to retrieve
269     * @param name name of the configuration parameter
270     * @return the parameter value
271     */
272    @SuppressWarnings("unchecked")
273    public <T extends Object> T getValue(String name)
274    {
275        DefinitionAndValue definitionAndValue = _definitionAndValues.get(name);
276        if (definitionAndValue == null)
277        {
278            __logger.warn("The configuration parameter {} doesn't exist. A null value is retrieved.", name);
279            return null;
280        }
281        
282        return (T) definitionAndValue.getValue();
283    }
284    
285    /**
286     * Retrieves the typed parameter value, or the default value
287     * @param name name of the configuration parameter
288     * @param useDefaultFromModel true to use the default value from the model, false to use the give default value
289     * @param defaultValue default value used if value is null and useDefaultFromModel is false, or if there is no default value on model
290     * @param <T> type of the value to retrieve
291     * @return the parameter value
292     */
293    public <T> T getValue(String name, boolean useDefaultFromModel, T defaultValue)
294    {
295        DefinitionAndValue definitionAndValue = _definitionAndValues.get(name);
296        if (definitionAndValue == null)
297        {
298            __logger.warn("The configuration parameter {} doesn't exist. The given default value is retrieved.", name);
299            return defaultValue;
300        }
301        
302        @SuppressWarnings("unchecked")
303        T value = (T) definitionAndValue.getValue();
304        if (value != null)
305        {
306            return value;
307        }
308        
309        if (useDefaultFromModel)
310        {
311            ElementDefinition definition = (ElementDefinition) definitionAndValue.getDefinition();
312            @SuppressWarnings("unchecked")
313            T defaultFromModel = (T) definition.getDefaultValue();
314            if (defaultFromModel != null)
315            {
316                return defaultFromModel;
317            }
318            __logger.debug("There is no default value in model for the configuration parameter {}. The given default value is retrieved.", name);
319        }
320        
321        return defaultValue;
322    }
323    
324    /**
325     * Dispose this {@link Config} instance
326     */
327    public static void dispose()
328    {
329        __config = null;
330    }
331}