001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.common.utils;
024
025import java.io.File;
026import java.io.InputStream;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.List;
030
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import dk.netarkivet.common.exceptions.ArgumentNotValid;
035import dk.netarkivet.common.exceptions.IOFailure;
036import dk.netarkivet.common.exceptions.UnknownID;
037
038/**
039 * Provides access to general application settings. The settings are retrieved from xml files. XML files may be
040 * specified one of two places: 1) Default settings in XML files, specified by class path. These are intended to be
041 * packaged in the jar files, to provide a fallback for settings. 2) Overriding settings in XML files in file systems.
042 * These are intended to override the necessary values with minimal XML files. The location of these files are either
043 * specified by the system property {@link #SETTINGS_FILE_PROPERTY}, multiple files can be separated by
044 * {@link File#pathSeparator}, that is ':' on linux and ';' on windows; or if that property is not set, the default
045 * location is {@link #DEFAULT_SETTINGS_FILEPATH}.
046 */
047public class Settings {
048
049    /** Logger for this class. */
050    private static final Logger log = LoggerFactory.getLogger(Settings.class);
051
052    /**
053     * The objects representing the contents of the settings xml files. For handling multithreaded instances this list
054     * must be initialised through the method Collections.synchronizedList().
055     */
056    private static final List<SimpleXml> fileSettingsXmlList;
057
058    /**
059     * The objects representing the contents of the default settings xml files in classpath. For handling multithreaded
060     * instances this list must be initialised through the method Collections.synchronizedList().
061     */
062    private static final List<SimpleXml> defaultClasspathSettingsXmlList;
063
064    static {
065        // All static initialization in one place
066        fileSettingsXmlList = Collections.synchronizedList(new ArrayList<SimpleXml>());
067        defaultClasspathSettingsXmlList = Collections.synchronizedList(new ArrayList<SimpleXml>());
068        // Perform an initial loading of the settings.
069        reload();
070    }
071
072    /**
073     * This system property specifies alternative position(s) to look for settings files. If more files are specified,
074     * they should be separated by {@link File#pathSeparatorChar}
075     */
076    public static final String SETTINGS_FILE_PROPERTY = "dk.netarkivet.settings.file";
077
078    /**
079     * The file path to look for settings in, if the system property {@link #SETTINGS_FILE_PROPERTY} is not set.
080     */
081    public static final String DEFAULT_SETTINGS_FILEPATH = "conf/settings.xml";
082
083    /** The newest "last modified" date of all settings files. */
084    private static long lastModified;
085
086    /**
087     * Return the file these settings are read from. If the property given in the constructor is set, that will be used
088     * to determine the file. If it is not set, the default settings file path given in the constructor will be used.
089     *
090     * @return The settings file.
091     */
092    public static List<File> getSettingsFiles() {
093        String[] pathList = System.getProperty(SETTINGS_FILE_PROPERTY, DEFAULT_SETTINGS_FILEPATH).split(
094                File.pathSeparator);
095        List<File> result = new ArrayList<File>();
096        for (String path : pathList) {
097            if (path.trim().length() != 0) {
098                File settingsFile = new File(path);
099                if (settingsFile.isFile()) {
100                    result.add(settingsFile);
101                }
102            }
103        }
104        return result;
105    }
106
107    /**
108     * Gets a setting. The search order for a given setting is as follows:
109     * <p>
110     * First it is checked, if the argument key is set as a System property. If yes, return this value. If no, we
111     * continue the search.
112     * <p>
113     * Secondly, we check, if the setting is in one of the loaded settings xml files. If the value is there, it is
114     * returned. If no, we continue the search.
115     * <p>
116     * Finally, we check if the setting is in one of default settings files from classpath. If the value is there, it is
117     * returned. Otherwise an UnknownId exception is thrown.
118     * <p>
119     * Note: The retrieved value can be the empty string
120     *
121     * @param key name of the setting to retrieve
122     * @return the retrieved value
123     * @throws ArgumentNotValid if key is null or the empty string
124     * @throws UnknownID if no setting loaded matches key
125     * @throws IOFailure if IO Failure
126     */
127    public static String get(String key) throws UnknownID, IOFailure, ArgumentNotValid {
128        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
129        String val = System.getProperty(key);
130        if (val != null) {
131            return val;
132        }
133
134        // Key not in System.properties try loaded data instead
135        synchronized (fileSettingsXmlList) {
136            for (SimpleXml settingsXml : fileSettingsXmlList) {
137                if (settingsXml.hasKey(key)) {
138                    return settingsXml.getString(key);
139                }
140            }
141        }
142
143        // Key not in file based settings, try classpath settings instead
144        synchronized (defaultClasspathSettingsXmlList) {
145            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
146                if (settingsXml.hasKey(key)) {
147                    return settingsXml.getString(key);
148                }
149            }
150        }
151        throw new UnknownID("No match for key '" + key + "' in settings");
152    }
153
154    /**
155     * Gets a setting as an int. This method calls get(key) and then parses the value as integer.
156     *
157     * @param key name of the setting to retrieve
158     * @return the retrieved int
159     * @throws ArgumentNotValid if key is null, the empty string or key is not parseable as an integer
160     * @throws UnknownID if no setting loaded matches key
161     */
162    public static int getInt(String key) throws UnknownID, ArgumentNotValid {
163        String value = get(key);
164        try {
165            return Integer.parseInt(value);
166        } catch (NumberFormatException e) {
167            String msg = "Invalid setting. Value '" + value + "' for key '" + key
168                    + "' could not be parsed as an integer.";
169            throw new ArgumentNotValid(msg, e);
170        }
171    }
172
173    /**
174     * Gets a setting as a long. This method calls get(key) and then parses the value as a long.
175     *
176     * @param key name of the setting to retrieve
177     * @return the retrieved long
178     * @throws ArgumentNotValid if key is null, the empty string or key is not parseable as a long
179     * @throws UnknownID if no setting loaded matches key
180     */
181    public static long getLong(String key) throws UnknownID, ArgumentNotValid {
182        String value = get(key);
183        try {
184            return Long.parseLong(value);
185        } catch (NumberFormatException e) {
186            String msg = "Invalid setting. Value '" + value + "' for key '" + key + "' could not be parsed as a long.";
187            throw new ArgumentNotValid(msg, e);
188        }
189    }
190
191    /**
192     * Gets a setting as a double. This method calls get(key) and then parses the value as a double.
193     *
194     * @param key name of the setting to retrieve
195     * @return the retrieved double
196     * @throws ArgumentNotValid if key is null, the empty string or key is not parseable as a double
197     * @throws UnknownID if no setting loaded matches key
198     */
199    public static double getDouble(String key) throws UnknownID, ArgumentNotValid {
200        String value = get(key);
201        try {
202            return Double.parseDouble(value);
203        } catch (NumberFormatException e) {
204            String msg = "Invalid setting. Value '" + value + "' for key '" + key
205                    + "' could not be parsed as a double.";
206            throw new ArgumentNotValid(msg, e);
207        }
208    }
209
210    /**
211     * Gets a setting as a file. This method calls get(key) and then returns the value as a file.
212     *
213     * @param key name of the setting to retrieve
214     * @return the retrieved file
215     * @throws ArgumentNotValid if key is null, the empty string
216     * @throws UnknownID if no setting loaded matches ke
217     */
218    public static File getFile(String key) {
219        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
220        return new File(get(key));
221    }
222
223    /**
224     * Gets a setting as a boolean. This method calls get(key) and then parses the value as a boolean.
225     *
226     * @param key name of the setting to retrieve
227     * @return the retrieved boolean
228     * @throws ArgumentNotValid if key is null or the empty string
229     * @throws UnknownID if no setting loaded matches key
230     */
231    public static boolean getBoolean(String key) throws UnknownID, ArgumentNotValid {
232        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
233        String value = get(key);
234        return Boolean.parseBoolean(value);
235    }
236
237    /**
238     * Gets a list of settings. First it is checked, if the key is registered as a System property. If yes, registered
239     * value is returned in a list of length 1. If no, the data loaded from the settings xml files are examined. If
240     * value is there, it is returned in a list. If not, the default settings from classpath are examined. If values for
241     * this setting are found here, they are returned. Otherwise, an UnknownId exception is thrown.
242     * <p>
243     * Note that the values will not be concatenated, the first place with a match will define the entire list.
244     * Furthemore the list cannot be empty.
245     *
246     * @param key name of the setting to retrieve
247     * @return the retrieved values (as a non-empty String array)
248     * @throws ArgumentNotValid if key is null or the empty string
249     * @throws UnknownID if no setting loaded matches key
250     */
251    public static String[] getAll(String key) throws UnknownID, ArgumentNotValid {
252        ArgumentNotValid.checkNotNullOrEmpty(key, "key");
253        String val = System.getProperty(key);
254        if (val != null) {
255            return new String[] {val};
256        }
257        if (fileSettingsXmlList.isEmpty()) {
258            System.out.print("The list of loaded data settings is empty. Is this OK?");
259        }
260        // Key not in System.properties try loaded data instead
261        synchronized (fileSettingsXmlList) {
262            for (SimpleXml settingsXml : fileSettingsXmlList) {
263                List<String> result = settingsXml.getList(key);
264                if (result.size() == 0) {
265                    continue;
266                }
267                if (log.isDebugEnabled()) {
268                    log.debug("Value found in loaded data: {}", StringUtils.conjoin(",", result));
269                }
270                return result.toArray(new String[result.size()]);
271            }
272        }
273
274        // Key not in file based settings, try settings from classpath
275        synchronized (defaultClasspathSettingsXmlList) {
276            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
277                List<String> result = settingsXml.getList(key);
278                if (result.size() == 0) {
279                    continue;
280                }
281                if (log.isDebugEnabled()) {
282                    log.debug("Value found in classpath data: {}", StringUtils.conjoin(",", result));
283                }
284                return result.toArray(new String[result.size()]);
285            }
286        }
287        throw new UnknownID("No match for key '" + key + "' in settings");
288    }
289
290    /**
291     * Sets the key to one or more values. Calls to this method are forgotten whenever the {@link #reload()} is
292     * executed.
293     * <p>
294     * TODO write these values to its own simpleXml structure, that are not reset during reload.
295     *
296     * @param key The settings key to add this under, legal keys are fields in this class.
297     * @param values The (ordered) list of values to put under this key.
298     * @throws ArgumentNotValid if key or values are null
299     * @throws UnknownID if the key does not already exist
300     */
301    public static void set(String key, String... values) {
302        ArgumentNotValid.checkNotNullOrEmpty(key, "key");
303        ArgumentNotValid.checkNotNull(values, "values");
304
305        if (fileSettingsXmlList.isEmpty()) {
306            fileSettingsXmlList.add(new SimpleXml("settings"));
307        }
308        SimpleXml simpleXml = fileSettingsXmlList.get(0);
309        if (simpleXml.hasKey(key)) {
310            simpleXml.update(key, values);
311        } else {
312            simpleXml.add(key, values);
313        }
314    }
315
316    /**
317     * Reloads the settings. This will reload the settings from disk, and forget all settings that were set with
318     * {@link #set}
319     * <p>
320     * The field {@link #lastModified} is updated to timestamp of the settings file that has been changed most recently.
321     *
322     * @throws IOFailure if settings cannot be loaded
323     */
324    public static synchronized void reload() {
325        lastModified = 0;
326        List<File> settingsFiles = getSettingsFiles();
327        List<SimpleXml> simpleXmlList = new ArrayList<SimpleXml>();
328        for (File settingsFile : settingsFiles) {
329            if (settingsFile.isFile()) {
330                simpleXmlList.add(new SimpleXml(settingsFile));
331            } else {
332                log.warn("The file '{}' is not a file, and therefore not loaded", settingsFile.getAbsolutePath());
333            }
334            if (settingsFile.lastModified() > lastModified) {
335                lastModified = settingsFile.lastModified();
336            }
337        }
338        synchronized (fileSettingsXmlList) {
339            fileSettingsXmlList.clear();
340            fileSettingsXmlList.addAll(simpleXmlList);
341        }
342    }
343
344    /**
345     * Add the settings file represented by this path to the list of default classpath settings.
346     *
347     * @param defaultClasspathSettingsPath the given default classpath setting.
348     */
349    public static void addDefaultClasspathSettings(String defaultClasspathSettingsPath) {
350        ArgumentNotValid.checkNotNullOrEmpty(defaultClasspathSettingsPath, "String defaultClasspathSettingsPath");
351        InputStream stream = Thread.currentThread().getContextClassLoader()
352                .getResourceAsStream(defaultClasspathSettingsPath);
353        if (stream != null) {
354            defaultClasspathSettingsXmlList.add(new SimpleXml(stream));
355        } else {
356            log.warn("Unable to read the settings file represented by path: '{}'", defaultClasspathSettingsPath);
357        }
358    }
359
360    /**
361     * Get a tree view of a part of the settings. Note: settings read with this mechanism do not support overriding with
362     * system properties!
363     *
364     * @param path Dotted path to a unique element in the tree.
365     * @return The part of the setting structure below the element given.
366     */
367    public static StringTree<String> getTree(String path) {
368        synchronized (fileSettingsXmlList) {
369            for (SimpleXml settingsXml : fileSettingsXmlList) {
370                if (settingsXml.hasKey(path)) {
371                    return settingsXml.getTree(path);
372                }
373            }
374        }
375
376        // Key not in file based settings, try classpath settings instead
377        synchronized (defaultClasspathSettingsXmlList) {
378            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
379                if (settingsXml.hasKey(path)) {
380                    return settingsXml.getTree(path);
381                }
382            }
383        }
384        throw new UnknownID("No match for key '" + path + "' in settings");
385    }
386
387}