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     * Gets the configuration model
104     * @return the model or null if the configuration is not initialized
105     */
106    public static Model getModel()
107    {
108        return __model;
109    }
110    
111    /**
112     * Sets the configuration model
113     * @param model the model to set
114     */
115    public static void setModel(Model model)
116    {
117        __model = model;
118    }
119    
120    private void _load() throws Exception
121    {
122        __logger.info("Loading configuration values from file {}.", __filename);
123        _definitionAndValues = __read();
124    }
125    
126    private void _reloadIfNeeded()
127    {
128        if (__fileExists && new File(__filename).exists() && __lastModified < new File(__filename).lastModified())
129        {
130            try
131            {
132                __logger.info("The config file has changed. Let's reload.");
133                _load();
134            }
135            catch (Exception e)
136            {
137                // __lastModified was changed, so we will not fail several times
138                // _values was not modified
139                // __fileExists is still true
140                __logger.error("The config file '" + __filename + "' was modified but could not be reloaded due to an exception", e);
141            }
142        }
143    }
144    
145    /**
146     * Get the last modification of the configuration file
147     * @return The last modified or -1 if the file does not exists
148     */
149    public long getLastModified()
150    {
151        return __lastModified;
152    }
153    
154    /**
155     * Read configuration file
156     * @return the configuration definition and values
157     * @throws Exception if a problem occurs reading values
158     */
159    static Map<String, DefinitionAndValue> __read() throws Exception
160    {
161        Map<String, DefinitionAndValue> definitionAndValues = new HashMap<>();
162
163        File configFile = new File(__filename);
164        __fileExists = configFile.exists();
165
166        if (__fileExists)
167        {
168            __lastModified  = configFile.lastModified();
169
170            Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(configFile);
171            for (Configuration parameter : configuration.getChildren())
172            {
173                String parameterName = parameter.getName();
174                if (__model.hasModelItem(parameterName))
175                {
176                    ModelItem modelItem = __model.getModelItem(parameterName);
177    
178                    if (modelItem instanceof ElementDefinition)
179                    {
180                        ElementDefinition definition = (ElementDefinition) modelItem;
181                        ElementType type = definition.getType();
182                        
183                        if (type instanceof XMLElementType)
184                        {
185                            Object value = ((XMLElementType) type).read(configuration, parameterName);
186                            DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, definition, value);
187                            definitionAndValues.put(parameterName, definitionAndValue);
188                        }
189                    }
190                }
191                else
192                {
193                    __logger.warn("The parameter {} is not defined in configuration. This parameter is ignored.", parameterName);
194                }
195            }
196        }
197        else
198        {
199            __lastModified = -1;
200        }
201
202        return definitionAndValues;
203    }
204    
205    /**
206     * Set the initialization status of the configuration
207     * @param initialized the initialization status of the configuration
208     */
209    public static void setInitialized(boolean initialized)
210    {
211        __initialized = initialized;
212    }
213
214    /**
215     * Set the configuration filename
216     * @param filename Name with path of the configuration file
217     */
218    public static void setFilename(String filename)
219    {
220        __filename = filename;
221        __fileExists = new File(__filename).exists();
222    }
223    
224    /**
225     * Returns true if the configuration filename exists.
226     * @return true if the configuration filename exists.
227     */
228    public static boolean fileExists()
229    {
230        return __fileExists;
231    }
232    
233    /**
234     * Retrieves all configuration parameter values
235     * @return the configuration values
236     */
237    public Map<String, Object> getValues()
238    {
239        return __extractValues(_definitionAndValues);
240    }
241    
242    static Map<String, Object> __extractValues(Map<String, DefinitionAndValue> definitionAndValues)
243    {
244        return DefinitionAndValue.extractValues(definitionAndValues);
245    }
246    
247    /**
248     * Retrieves all configuration parameter values as JSON objects
249     * @return the configuration values as JSON objects
250     */
251    public static Map<String, Object> getValuesAsJSONForClient()
252    {
253        Map<String, Object> jsonValues = new HashMap<>();
254        
255        Set<Entry<String, DefinitionAndValue>> values;
256        
257        try
258        {
259            values = __read().entrySet();
260        }
261        catch (Exception e)
262        {
263            __logger.warn("Config values are unreadable. Using default values", e);
264            return jsonValues;
265        }
266        
267        for (Map.Entry<String, DefinitionAndValue> entry : values)
268        {
269            DefinitionAndValue definitionAndValue = entry.getValue();
270            
271            Object value = definitionAndValue.getValue();
272            ElementDefinition definition = (ElementDefinition) definitionAndValue.getDefinition();
273            
274            ElementType<Object> type = definition.getType();
275            Object jsonValue = type.valueToJSONForClient(value, DataContext.newInstance());
276            
277            jsonValues.put(entry.getKey(), jsonValue);
278        }
279        
280        return jsonValues;
281        
282    }
283    
284    /**
285     * Retrieves the typed parameter value
286     * @param <T> type of the value to retrieve
287     * @param name name of the configuration parameter
288     * @return the parameter value
289     */
290    @SuppressWarnings("unchecked")
291    public <T extends Object> T getValue(String name)
292    {
293        DefinitionAndValue definitionAndValue = _definitionAndValues.get(name);
294        if (definitionAndValue == null)
295        {
296            __logger.warn("The configuration parameter {} doesn't exist. A null value is retrieved.", name);
297            return null;
298        }
299        
300        return (T) definitionAndValue.getValue();
301    }
302    
303    /**
304     * Retrieves the typed parameter value, or the default value
305     * @param name name of the configuration parameter
306     * @param useDefaultFromModel true to use the default value from the model, false to use the give default value
307     * @param defaultValue default value used if value is null and useDefaultFromModel is false, or if there is no default value on model
308     * @param <T> type of the value to retrieve
309     * @return the parameter value
310     */
311    public <T> T getValue(String name, boolean useDefaultFromModel, T defaultValue)
312    {
313        DefinitionAndValue definitionAndValue = _definitionAndValues.get(name);
314        if (definitionAndValue == null)
315        {
316            __logger.warn("The configuration parameter {} doesn't exist. The given default value is retrieved.", name);
317            return defaultValue;
318        }
319        
320        @SuppressWarnings("unchecked")
321        T value = (T) definitionAndValue.getValue();
322        if (value != null)
323        {
324            return value;
325        }
326        
327        if (useDefaultFromModel)
328        {
329            ElementDefinition definition = (ElementDefinition) definitionAndValue.getDefinition();
330            @SuppressWarnings("unchecked")
331            T defaultFromModel = (T) definition.getDefaultValue();
332            if (defaultFromModel != null)
333            {
334                return defaultFromModel;
335            }
336            __logger.debug("There is no default value in model for the configuration parameter {}. The given default value is retrieved.", name);
337        }
338        
339        return defaultValue;
340    }
341    
342    /**
343     * Dispose this {@link Config} instance
344     */
345    public static void dispose()
346    {
347        __config = null;
348    }
349}