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