<commit_before>/* $Id$
 * $Revision$
 * $Date$
 * $Author$
 *
 * The Netarchive Suite - Software to harvest and preserve websites
 * Copyright 2004-2012 The Royal Danish Library, the Danish State and
 * University Library, the National Library of France and the Austrian
 * National Library.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package dk.netarkivet.common.utils;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dk.netarkivet.common.exceptions.ArgumentNotValid;
import dk.netarkivet.common.exceptions.IOFailure;
import dk.netarkivet.common.exceptions.UnknownID;

/**
 * Provides access to general application settings. The settings are retrieved
 * from xml files. XML files may be specified one of two places: 1) Default
 * settings in XML files, specified by class path. These are intended to be
 * packaged in the jar files, to provide a fallback for settings. 2) Overriding
 * settings in XML files in file systems. These are intended to override the
 * necessary values with minimal XML files. The location of these files are
 * either specified by the system property {@link #SETTINGS_FILE_PROPERTY},
 * multiple files can be separated by {@link File#pathSeparator}, that is ':' on
 * linux and ';' on windows; or if that property is not set, the default
 * location is {@link #DEFAULT_SETTINGS_FILEPATH}.
 */
public class Settings {

    /**
     * The objects representing the contents of the settings xml files. For
     * handling multithreaded instances this list must be initialised through
     * the method Collections.synchronizedList().
     */
    private static final List<SimpleXml> fileSettingsXmlList
            = Collections.synchronizedList(new ArrayList<SimpleXml>());
    static {

        // Perform an initial loading of the settings.
        reload();
    }

    /** Logger for this class. */
    private static final Log log
            = LogFactory.getLog(Settings.class.getName());


    /**
     * The objects representing the contents of the default settings xml files
     * in classpath. For handling multithreaded instances this list must be
     * initialised through the method Collections.synchronizedList().
     */
    private static final List<SimpleXml> defaultClasspathSettingsXmlList
            = Collections.synchronizedList(new ArrayList<SimpleXml>());
    /**
     * This system property specifies alternative position(s) to look for
     * settings files. If more files are specified, they should be separated by
     * {@link File#pathSeparatorChar}
     */
    public static final String SETTINGS_FILE_PROPERTY
            = "dk.netarkivet.settings.file";

    /**
     * The file path to look for settings in, if the system property {@link
     * #SETTINGS_FILE_PROPERTY} is not set.
     */
    public static final String DEFAULT_SETTINGS_FILEPATH = "conf/settings.xml";
    /** The newest "last modified" date of all settings files. */
    private static long lastModified;

    /**
     * Return the file these settings are read from. If the property given in
     * the constructor is set, that will be used to determine the file. If it is
     * not set, the default settings file path given in the constructor will be
     * used.
     *
     * @return The settings file.
     */
    public static List<File> getSettingsFiles() {
        String[] pathList = System.getProperty(SETTINGS_FILE_PROPERTY,
                                               DEFAULT_SETTINGS_FILEPATH).split(
                File.pathSeparator);
        List<File> result = new ArrayList<File>();
        for (String path : pathList) {
            if (path.trim().length() != 0) {
                File settingsFile = new File(path);
                if (settingsFile.isFile()) {
                    result.add(settingsFile);
                }
            }
        }
        return result;
    }

    /**
     * Gets a setting. The search order for a given setting is as follows:
     *
     * First it is checked, if the argument key is set as a System property. If
     * yes, return this value. If no, we continue the search.
     *
     * Secondly, we check, if the setting is in one of the loaded settings xml
     * files. If the value is there, it is returned. If no, we continue the
     * search.
     *
     * Finally, we check if the setting is in one of default settings files from
     * classpath. If the value is there, it is returned. Otherwise an UnknownId
     * exception is thrown.
     *
     * Note: The retrieved value can be the empty string
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved value
     *
     * @throws ArgumentNotValid if key is null or the empty string
     * @throws UnknownID        if no setting loaded matches key
     * @throws IOFailure        if IO Failure
     */
    public static String get(String key)
            throws UnknownID, IOFailure, ArgumentNotValid {
        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
        String val = System.getProperty(key);
        if (val != null) {
            return val;
        }

        // Key not in System.properties try loaded data instead
        synchronized (fileSettingsXmlList) {
            for (SimpleXml settingsXml : fileSettingsXmlList) {
                if (settingsXml.hasKey(key)) {
                    return settingsXml.getString(key);
                }
            }
        }

        // Key not in file based settings, try classpath settings instead
        synchronized (defaultClasspathSettingsXmlList) {
            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
                if (settingsXml.hasKey(key)) {
                    return settingsXml.getString(key);
                }
            }
        }
        throw new UnknownID("No match for key '" + key + "' in settings");
    }

    /**
     * Gets a setting as an int. This method calls get(key) and then parses the
     * value as integer.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved int
     *
     * @throws ArgumentNotValid if key is null, the empty string or key is not
     *                          parseable as an integer
     * @throws UnknownID        if no setting loaded matches key
     */
    public static int getInt(String key)
            throws UnknownID, ArgumentNotValid {
        String value = get(key);
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            String msg = "Invalid setting. Value '" + value + "' for key '"
                         + key + "' could not be parsed as an integer.";
            throw new ArgumentNotValid(msg, e);
        }
    }

    /**
     * Gets a setting as a long. This method calls get(key) and then parses the
     * value as a long.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved long
     *
     * @throws ArgumentNotValid if key is null, the empty string or key is not
     *                          parseable as a long
     * @throws UnknownID        if no setting loaded matches key
     */
    public static long getLong(String key)
            throws UnknownID, ArgumentNotValid {
        String value = get(key);
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            String msg = "Invalid setting. Value '" + value + "' for key '"
                         + key + "' could not be parsed as a long.";
            throw new ArgumentNotValid(msg, e);
        }
    }
    
    /**
     * Gets a setting as a double. This method calls get(key) and then parses the
     * value as a double.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved double
     *
     * @throws ArgumentNotValid if key is null, the empty string or key is not
     *                          parseable as a double
     * @throws UnknownID        if no setting loaded matches key
     */
    public static double getDouble(String key)
            throws UnknownID, ArgumentNotValid {
        String value = get(key);
        try {
            return Double.parseDouble(value);
        } catch (NumberFormatException e) {
            String msg = "Invalid setting. Value '" + value + "' for key '"
                         + key + "' could not be parsed as a double.";
            throw new ArgumentNotValid(msg, e);
        }
    }

    /**
     * Gets a setting as a file. This method calls get(key) and then returns the
     * value as a file.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved file
     *
     * @throws ArgumentNotValid if key is null, the empty string
     * @throws UnknownID        if no setting loaded matches ke
     */
    public static File getFile(String key) {
        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
        return new File(get(key));
    }

    /**
     * Gets a setting as a boolean. This method calls get(key) and then parses
     * the value as a boolean.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved boolean
     *
     * @throws ArgumentNotValid if key is null or the empty string
     * @throws UnknownID        if no setting loaded matches key
     */
    public static boolean getBoolean(String key)
            throws UnknownID, ArgumentNotValid {
        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
        String value = get(key);
        return Boolean.parseBoolean(value);
    }

    /**
     * Gets a list of settings. First it is checked, if the key is registered as
     * a System property. If yes, registered value is returned in a list of
     * length 1. If no, the data loaded from the settings xml files are
     * examined. If value is there, it is returned in a list. If not, the
     * default settings from classpath are examined. If values for this setting
     * are found here, they are returned. Otherwise, an UnknownId exception is
     * thrown.
     *
     * Note that the values will not be concatenated, the first place with a
     * match will define the entire list. Furthemore the list cannot be empty.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved values (as a non-empty String array)
     *
     * @throws ArgumentNotValid if key is null or the empty string
     * @throws UnknownID        if no setting loaded matches key
     */
    public static String[] getAll(String key)
            throws UnknownID, ArgumentNotValid {
        ArgumentNotValid.checkNotNullOrEmpty(key, "key");
        log.debug("Searching for a setting for key: " + key);
        String val = System.getProperty(key);
        if (val != null) {
            log.debug("value for key found in property:" + val);
            return new String[]{val};
        }
        if (fileSettingsXmlList.isEmpty()) {
            log.warn("The list of loaded data settings is empty."
                     + "Is this OK?");
        }
        // Key not in System.properties try loaded data instead
        synchronized (fileSettingsXmlList) {
            for (SimpleXml settingsXml : fileSettingsXmlList) {
                List<String> result
                        = settingsXml.getList(key);
                if (result.size() == 0) {
                    continue;
                }
                log.debug("Value found in loaded data: "
                          + StringUtils.conjoin(",", result));
                return result.toArray(new String[result.size()]);
            }
        }

        // Key not in file based settings, try settings from classpath
        synchronized (defaultClasspathSettingsXmlList) {
            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
                List<String> result
                        = settingsXml.getList(key);
                if (result.size() == 0) {
                    continue;
                }
                log.debug("Value found in classpath data: "
                          + StringUtils.conjoin(",", result));
                return result.toArray(new String[result.size()]);
            }
        }
        throw new UnknownID("No match for key '" + key + "' in settings");
    }

    /**
     * Sets the key to one or more values. Calls to this method are forgotten
     * whenever the {@link #reload()} is executed.
     *
     * TODO write these values to its own simpleXml structure, that are not
     * reset during reload.
     *
     * @param key    The settings key to add this under, legal keys are fields
     *               in this class.
     * @param values The (ordered) list of values to put under this key.
     *
     * @throws ArgumentNotValid if key or values are null
     * @throws UnknownID        if the key does not already exist
     */
    public static void set(String key, String... values) {
        ArgumentNotValid.checkNotNullOrEmpty(key, "key");
        ArgumentNotValid.checkNotNull(values, "values");

        if (fileSettingsXmlList.isEmpty()) {
            fileSettingsXmlList.add(new SimpleXml("settings"));
        }
        SimpleXml simpleXml = fileSettingsXmlList.get(0);
        if (simpleXml.hasKey(key)) {
            simpleXml.update(key, values);
        } else {
            simpleXml.add(key, values);
        }
    }

    /**
     * Reload the settings if they have changed on disk. This behaves exactly as
     * forceReload, except it only reloads if the data of the file is different
     * than last time it was loaded.
     *
     * @throws IOFailure if settings cannot be loaded
     */
    public static synchronized void conditionalReload() {
        List<File> settingsFiles = getSettingsFiles();
        for (File settingsFile : settingsFiles) {
            if (settingsFile.lastModified() > lastModified) {
                log.info("Do reload of settings, as the file '"
                         + settingsFile.getAbsolutePath()
                         + "' has changed since last reload");
                reload();
                return;
            }
        }
    }

    /**
     * Reloads the settings. This will reload the settings from disk, and forget
     * all settings that were set with {@link #set}
     *
     * The field {@link #lastModified} is updated to timestamp of the settings
     * file that has been changed most recently.
     *
     * @throws IOFailure if settings cannot be loaded
     * @see #conditionalReload()
     */
    public static synchronized void reload() {
        lastModified = 0;
        List<File> settingsFiles = getSettingsFiles();
        List<SimpleXml> simpleXmlList = new ArrayList<SimpleXml>();
        for (File settingsFile : settingsFiles) {
            if (settingsFile.isFile()) {
                simpleXmlList.add(new SimpleXml(settingsFile));
            } else {
                log.warn("The file '"
                         + settingsFile.getAbsolutePath()
                         + "' is not a file, and therefore not loaded");
            }
            if (settingsFile.lastModified() > lastModified) {
                lastModified = settingsFile.lastModified();
            }
        }
        synchronized (fileSettingsXmlList) {
            fileSettingsXmlList.clear();
            fileSettingsXmlList.addAll(simpleXmlList);
        }
    }

    /**
     * Add the settings file represented by this path to the list of default
     * classpath settings.
     *
     * @param defaultClasspathSettingsPath the given default classpath setting.
     */
    public static void addDefaultClasspathSettings(
            String defaultClasspathSettingsPath) {
        ArgumentNotValid.checkNotNullOrEmpty(
                defaultClasspathSettingsPath,
                "String defaultClasspathSettingsPath");
        InputStream stream = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream(defaultClasspathSettingsPath);
        if (stream != null) {
            defaultClasspathSettingsXmlList.add(new SimpleXml(stream));
        } else {
            log.warn("Unable to read the settings file represented by path: '"
                     + defaultClasspathSettingsPath + "'");
        }
    }

    /**
     * Get a tree view of a part of the settings. Note: settings read with this
     * mechanism do not support overriding with system properties!
     *
     * @param path Dotted path to a unique element in the tree.
     *
     * @return The part of the setting structure below the element given.
     */
    public static StringTree<String> getTree(String path) {
        synchronized (fileSettingsXmlList) {
            for (SimpleXml settingsXml : fileSettingsXmlList) {
                if (settingsXml.hasKey(path)) {
                    return settingsXml.getTree(path);
                }
            }
        }

        // Key not in file based settings, try classpath settings instead
        synchronized (defaultClasspathSettingsXmlList) {
            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
                if (settingsXml.hasKey(path)) {
                    return settingsXml.getTree(path);
                }
            }
        }
        throw new UnknownID("No match for key '" + path + "' in settings");
    }

}
<commit_msg>NAS-2218 NullPointerException in Settings static initializer<commit_after>/* $Id$
 * $Revision$
 * $Date$
 * $Author$
 *
 * The Netarchive Suite - Software to harvest and preserve websites
 * Copyright 2004-2012 The Royal Danish Library, the Danish State and
 * University Library, the National Library of France and the Austrian
 * National Library.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package dk.netarkivet.common.utils;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dk.netarkivet.common.exceptions.ArgumentNotValid;
import dk.netarkivet.common.exceptions.IOFailure;
import dk.netarkivet.common.exceptions.UnknownID;

/**
 * Provides access to general application settings. The settings are retrieved
 * from xml files. XML files may be specified one of two places: 1) Default
 * settings in XML files, specified by class path. These are intended to be
 * packaged in the jar files, to provide a fallback for settings. 2) Overriding
 * settings in XML files in file systems. These are intended to override the
 * necessary values with minimal XML files. The location of these files are
 * either specified by the system property {@link #SETTINGS_FILE_PROPERTY},
 * multiple files can be separated by {@link File#pathSeparator}, that is ':' on
 * linux and ';' on windows; or if that property is not set, the default
 * location is {@link #DEFAULT_SETTINGS_FILEPATH}.
 */
public class Settings {

    /**
     * The objects representing the contents of the settings xml files. For
     * handling multithreaded instances this list must be initialised through
     * the method Collections.synchronizedList().
     */
    private static final List<SimpleXml> fileSettingsXmlList;

    /**
     * The objects representing the contents of the default settings xml files
     * in classpath. For handling multithreaded instances this list must be
     * initialised through the method Collections.synchronizedList().
     */
    private static final List<SimpleXml> defaultClasspathSettingsXmlList;
    
    static {
    	// All static initialization in one place
    	fileSettingsXmlList = Collections.synchronizedList(new ArrayList<SimpleXml>());
    	defaultClasspathSettingsXmlList = Collections.synchronizedList(new ArrayList<SimpleXml>());    	
        // Perform an initial loading of the settings.
        reload();
    }

    /** Logger for this class. */
    private static final Log log
            = LogFactory.getLog(Settings.class.getName());
    
    /**
     * This system property specifies alternative position(s) to look for
     * settings files. If more files are specified, they should be separated by
     * {@link File#pathSeparatorChar}
     */
    public static final String SETTINGS_FILE_PROPERTY
            = "dk.netarkivet.settings.file";

    /**
     * The file path to look for settings in, if the system property {@link
     * #SETTINGS_FILE_PROPERTY} is not set.
     */
    public static final String DEFAULT_SETTINGS_FILEPATH = "conf/settings.xml";
    /** The newest "last modified" date of all settings files. */
    private static long lastModified;

    /**
     * Return the file these settings are read from. If the property given in
     * the constructor is set, that will be used to determine the file. If it is
     * not set, the default settings file path given in the constructor will be
     * used.
     *
     * @return The settings file.
     */
    public static List<File> getSettingsFiles() {
        String[] pathList = System.getProperty(SETTINGS_FILE_PROPERTY,
                                               DEFAULT_SETTINGS_FILEPATH).split(
                File.pathSeparator);
        List<File> result = new ArrayList<File>();
        for (String path : pathList) {
            if (path.trim().length() != 0) {
                File settingsFile = new File(path);
                if (settingsFile.isFile()) {
                    result.add(settingsFile);
                }
            }
        }
        return result;
    }

    /**
     * Gets a setting. The search order for a given setting is as follows:
     *
     * First it is checked, if the argument key is set as a System property. If
     * yes, return this value. If no, we continue the search.
     *
     * Secondly, we check, if the setting is in one of the loaded settings xml
     * files. If the value is there, it is returned. If no, we continue the
     * search.
     *
     * Finally, we check if the setting is in one of default settings files from
     * classpath. If the value is there, it is returned. Otherwise an UnknownId
     * exception is thrown.
     *
     * Note: The retrieved value can be the empty string
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved value
     *
     * @throws ArgumentNotValid if key is null or the empty string
     * @throws UnknownID        if no setting loaded matches key
     * @throws IOFailure        if IO Failure
     */
    public static String get(String key)
            throws UnknownID, IOFailure, ArgumentNotValid {
        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
        String val = System.getProperty(key);
        if (val != null) {
            return val;
        }

        // Key not in System.properties try loaded data instead
        synchronized (fileSettingsXmlList) {
            for (SimpleXml settingsXml : fileSettingsXmlList) {
                if (settingsXml.hasKey(key)) {
                    return settingsXml.getString(key);
                }
            }
        }

        // Key not in file based settings, try classpath settings instead
        synchronized (defaultClasspathSettingsXmlList) {
            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
                if (settingsXml.hasKey(key)) {
                    return settingsXml.getString(key);
                }
            }
        }
        throw new UnknownID("No match for key '" + key + "' in settings");
    }

    /**
     * Gets a setting as an int. This method calls get(key) and then parses the
     * value as integer.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved int
     *
     * @throws ArgumentNotValid if key is null, the empty string or key is not
     *                          parseable as an integer
     * @throws UnknownID        if no setting loaded matches key
     */
    public static int getInt(String key)
            throws UnknownID, ArgumentNotValid {
        String value = get(key);
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            String msg = "Invalid setting. Value '" + value + "' for key '"
                         + key + "' could not be parsed as an integer.";
            throw new ArgumentNotValid(msg, e);
        }
    }

    /**
     * Gets a setting as a long. This method calls get(key) and then parses the
     * value as a long.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved long
     *
     * @throws ArgumentNotValid if key is null, the empty string or key is not
     *                          parseable as a long
     * @throws UnknownID        if no setting loaded matches key
     */
    public static long getLong(String key)
            throws UnknownID, ArgumentNotValid {
        String value = get(key);
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            String msg = "Invalid setting. Value '" + value + "' for key '"
                         + key + "' could not be parsed as a long.";
            throw new ArgumentNotValid(msg, e);
        }
    }
    
    /**
     * Gets a setting as a double. This method calls get(key) and then parses the
     * value as a double.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved double
     *
     * @throws ArgumentNotValid if key is null, the empty string or key is not
     *                          parseable as a double
     * @throws UnknownID        if no setting loaded matches key
     */
    public static double getDouble(String key)
            throws UnknownID, ArgumentNotValid {
        String value = get(key);
        try {
            return Double.parseDouble(value);
        } catch (NumberFormatException e) {
            String msg = "Invalid setting. Value '" + value + "' for key '"
                         + key + "' could not be parsed as a double.";
            throw new ArgumentNotValid(msg, e);
        }
    }

    /**
     * Gets a setting as a file. This method calls get(key) and then returns the
     * value as a file.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved file
     *
     * @throws ArgumentNotValid if key is null, the empty string
     * @throws UnknownID        if no setting loaded matches ke
     */
    public static File getFile(String key) {
        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
        return new File(get(key));
    }

    /**
     * Gets a setting as a boolean. This method calls get(key) and then parses
     * the value as a boolean.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved boolean
     *
     * @throws ArgumentNotValid if key is null or the empty string
     * @throws UnknownID        if no setting loaded matches key
     */
    public static boolean getBoolean(String key)
            throws UnknownID, ArgumentNotValid {
        ArgumentNotValid.checkNotNullOrEmpty(key, "String key");
        String value = get(key);
        return Boolean.parseBoolean(value);
    }

    /**
     * Gets a list of settings. First it is checked, if the key is registered as
     * a System property. If yes, registered value is returned in a list of
     * length 1. If no, the data loaded from the settings xml files are
     * examined. If value is there, it is returned in a list. If not, the
     * default settings from classpath are examined. If values for this setting
     * are found here, they are returned. Otherwise, an UnknownId exception is
     * thrown.
     *
     * Note that the values will not be concatenated, the first place with a
     * match will define the entire list. Furthemore the list cannot be empty.
     *
     * @param key name of the setting to retrieve
     *
     * @return the retrieved values (as a non-empty String array)
     *
     * @throws ArgumentNotValid if key is null or the empty string
     * @throws UnknownID        if no setting loaded matches key
     */
    public static String[] getAll(String key)
            throws UnknownID, ArgumentNotValid {
        ArgumentNotValid.checkNotNullOrEmpty(key, "key");
        log.debug("Searching for a setting for key: " + key);
        String val = System.getProperty(key);
        if (val != null) {
            log.debug("value for key found in property:" + val);
            return new String[]{val};
        }
        if (fileSettingsXmlList.isEmpty()) {
            log.warn("The list of loaded data settings is empty."
                     + "Is this OK?");
        }
        // Key not in System.properties try loaded data instead
        synchronized (fileSettingsXmlList) {
            for (SimpleXml settingsXml : fileSettingsXmlList) {
                List<String> result
                        = settingsXml.getList(key);
                if (result.size() == 0) {
                    continue;
                }
                log.debug("Value found in loaded data: "
                          + StringUtils.conjoin(",", result));
                return result.toArray(new String[result.size()]);
            }
        }

        // Key not in file based settings, try settings from classpath
        synchronized (defaultClasspathSettingsXmlList) {
            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
                List<String> result
                        = settingsXml.getList(key);
                if (result.size() == 0) {
                    continue;
                }
                log.debug("Value found in classpath data: "
                          + StringUtils.conjoin(",", result));
                return result.toArray(new String[result.size()]);
            }
        }
        throw new UnknownID("No match for key '" + key + "' in settings");
    }

    /**
     * Sets the key to one or more values. Calls to this method are forgotten
     * whenever the {@link #reload()} is executed.
     *
     * TODO write these values to its own simpleXml structure, that are not
     * reset during reload.
     *
     * @param key    The settings key to add this under, legal keys are fields
     *               in this class.
     * @param values The (ordered) list of values to put under this key.
     *
     * @throws ArgumentNotValid if key or values are null
     * @throws UnknownID        if the key does not already exist
     */
    public static void set(String key, String... values) {
        ArgumentNotValid.checkNotNullOrEmpty(key, "key");
        ArgumentNotValid.checkNotNull(values, "values");

        if (fileSettingsXmlList.isEmpty()) {
            fileSettingsXmlList.add(new SimpleXml("settings"));
        }
        SimpleXml simpleXml = fileSettingsXmlList.get(0);
        if (simpleXml.hasKey(key)) {
            simpleXml.update(key, values);
        } else {
            simpleXml.add(key, values);
        }
    }

    /**
     * Reload the settings if they have changed on disk. This behaves exactly as
     * forceReload, except it only reloads if the data of the file is different
     * than last time it was loaded.
     *
     * @throws IOFailure if settings cannot be loaded
     */
    public static synchronized void conditionalReload() {
        List<File> settingsFiles = getSettingsFiles();
        for (File settingsFile : settingsFiles) {
            if (settingsFile.lastModified() > lastModified) {
                log.info("Do reload of settings, as the file '"
                         + settingsFile.getAbsolutePath()
                         + "' has changed since last reload");
                reload();
                return;
            }
        }
    }

    /**
     * Reloads the settings. This will reload the settings from disk, and forget
     * all settings that were set with {@link #set}
     *
     * The field {@link #lastModified} is updated to timestamp of the settings
     * file that has been changed most recently.
     *
     * @throws IOFailure if settings cannot be loaded
     * @see #conditionalReload()
     */
    public static synchronized void reload() {
        lastModified = 0;
        List<File> settingsFiles = getSettingsFiles();
        List<SimpleXml> simpleXmlList = new ArrayList<SimpleXml>();
        for (File settingsFile : settingsFiles) {
            if (settingsFile.isFile()) {
                simpleXmlList.add(new SimpleXml(settingsFile));
            } else {
                log.warn("The file '"
                         + settingsFile.getAbsolutePath()
                         + "' is not a file, and therefore not loaded");
            }
            if (settingsFile.lastModified() > lastModified) {
                lastModified = settingsFile.lastModified();
            }
        }
        synchronized (fileSettingsXmlList) {
            fileSettingsXmlList.clear();
            fileSettingsXmlList.addAll(simpleXmlList);
        }
    }

    /**
     * Add the settings file represented by this path to the list of default
     * classpath settings.
     *
     * @param defaultClasspathSettingsPath the given default classpath setting.
     */
    public static void addDefaultClasspathSettings(
            String defaultClasspathSettingsPath) {
        ArgumentNotValid.checkNotNullOrEmpty(
                defaultClasspathSettingsPath,
                "String defaultClasspathSettingsPath");
        InputStream stream = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream(defaultClasspathSettingsPath);
        if (stream != null) {
            defaultClasspathSettingsXmlList.add(new SimpleXml(stream));
        } else {
            log.warn("Unable to read the settings file represented by path: '"
                     + defaultClasspathSettingsPath + "'");
        }
    }

    /**
     * Get a tree view of a part of the settings. Note: settings read with this
     * mechanism do not support overriding with system properties!
     *
     * @param path Dotted path to a unique element in the tree.
     *
     * @return The part of the setting structure below the element given.
     */
    public static StringTree<String> getTree(String path) {
        synchronized (fileSettingsXmlList) {
            for (SimpleXml settingsXml : fileSettingsXmlList) {
                if (settingsXml.hasKey(path)) {
                    return settingsXml.getTree(path);
                }
            }
        }

        // Key not in file based settings, try classpath settings instead
        synchronized (defaultClasspathSettingsXmlList) {
            for (SimpleXml settingsXml : defaultClasspathSettingsXmlList) {
                if (settingsXml.hasKey(path)) {
                    return settingsXml.getTree(path);
                }
            }
        }
        throw new UnknownID("No match for key '" + path + "' in settings");
    }

}

{'fraction_non_alphanumeric': 0.07672935315211737, 'fraction_numerical': 0.0016971886890586078, 'mean_word_length': 2.076119905692152, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3823417', 'n_tokens_mistral': 9171, 'n_tokens_neox': 8956, 'n_words': 3571}