/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.velocity.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class extends normal Java properties by adding the possibility
 * to use the same key many times concatenating the value strings
 * instead of overwriting them.
 * <p>
 * <b>Please consider using the <code>PropertiesConfiguration</code> class in
 * Commons-Configuration as soon as it is released.</b>
 * <p>
 * The Extended Properties syntax is explained here:
 *
 * <ul>
 *  <li>
 *   Each property has the syntax <code>key = value</code>
 *  </li>
 *  <li>
 *   The <i>key</i> may use any character but the equal sign '='.
 *  </li>
 *  <li>
 *   <i>value</i> may be separated on different lines if a backslash
 *   is placed at the end of the line that continues below.
 *  </li>
 *  <li>
 *   If <i>value</i> is a list of strings, each token is separated
 *   by a comma ','.
 *  </li>
 *  <li>
 *   Commas in each token are escaped placing a backslash right before
 *   the comma.
 *  </li>
 *  <li>
 *   Backslashes are escaped by using two consecutive backslashes i.e. \\
 *  </li>
 *  <li>
 *   If a <i>key</i> is used more than once, the values are appended
 *   as if they were on the same line separated with commas.
 *  </li>
 *  <li>
 *   Blank lines and lines starting with character '#' are skipped.
 *  </li>
 *  <li>
 *   If a property is named "include" (or whatever is defined by
 *   setInclude() and getInclude() and the value of that property is
 *   the full path to a file on disk, that file will be included into
 *   the ConfigurationsRepository. You can also pull in files relative
 *   to the parent configuration file. So if you have something
 *   like the following:
 *
 *   include = additional.properties
 *
 *   Then "additional.properties" is expected to be in the same
 *   directory as the parent configuration file.
 *
 *   Duplicate name values will be replaced, so be careful.
 *
 *  </li>
 * </ul>
 *
 * <p>Here is an example of a valid extended properties file:</p>
 *
 * <pre><code>
 *      # lines starting with # are comments
 *
 *      # This is the simplest property
 *      key = value
 *
 *      # A long property may be separated on multiple lines
 *      longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
 *                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 *
 *      # This is a property with many tokens
 *      tokens_on_a_line = first token, second token
 *
 *      # This sequence generates exactly the same result
 *      tokens_on_multiple_lines = first token
 *      tokens_on_multiple_lines = second token
 *
 *      # commas may be escaped in tokens
 *      commas.escaped = Hi\, what'up?
 * </code></pre>
 *
 * <p><b>NOTE</b>: this class has <b>not</b> been written for
 * performance nor low memory usage.  In fact, it's way slower than it
 * could be and generates too much memory garbage.  But since
 * performance is not an issue during intialization (and there is not
 * much time to improve it), I wrote it this way.  If you don't like
 * it, go ahead and tune it up!
 *
 * This class is a clone of org.apache.commons.collections.ExtendedProperties
 * (which has been removed from commons-collections-4.x)
 *
 * @since 2.0
 * @version $Revision: $
 * @version $Id: ExtProperties.java$
 *
 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
 * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
 * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
 * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
 * @author Janek Bogucki
 * @author Mohan Kishore
 * @author Stephen Colebourne
 * @author Shinobu Kawai
 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
 * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
 */
@SuppressWarnings("deprecation")
public class ExtProperties extends DeprecationAwareExtProperties
{
    /**
     * Default configurations repository.
     */
    private ExtProperties defaults;

    /**
     * The file connected to this repository (holding comments and
     * such).
     *
     * @serial
     */
    protected String file;

    /**
     * Base path of the configuration file used to create
     * this ExtProperties object.
     */
    protected String basePath;

    /**
     * File separator.
     */
    protected String fileSeparator;
    {
        try
        {
            fileSeparator = (String) AccessController.doPrivileged(
                (PrivilegedAction) () -> System.getProperty("file.separator"));
        }
        catch (SecurityException ex)
        {
            fileSeparator = File.separator;
        }
    }

    /**
     * Has this configuration been initialized.
     */
    protected boolean isInitialized = false;

    /**
     * This is the name of the property that can point to other
     * properties file for including other properties files.
     */
    protected static String include = "include";

    /**
     * These are the keys in the order they listed
     * in the configuration file. This is useful when
     * you wish to perform operations with configuration
     * information in a particular order.
     */
    protected ArrayList<String> keysAsListed = new ArrayList<>();

    protected final static String START_TOKEN="${";
    protected final static String END_TOKEN="}";


    /**
     * Interpolate key names to handle ${key} stuff
     *
     * @param base string to interpolate
     * @return returns the key name with the ${key} substituted
     */
    protected String interpolate(String base)
    {
        // COPIED from [configuration] 2003-12-29
        return (interpolateHelper(base, null));
    }

    /**
     * Recursive handler for multiple levels of interpolation.
     *
     * When called the first time, priorVariables should be null.
     *
     * @param base string with the ${key} variables
     * @param priorVariables serves two purposes: to allow checking for
     * loops, and creating a meaningful exception message should a loop
     * occur.  It's 0'th element will be set to the value of base from
     * the first call.  All subsequent interpolated variables are added
     * afterward.
     *
     * @return the string with the interpolation taken care of
     */
    protected String interpolateHelper(String base, List<String> priorVariables)
    {
        // COPIED from [configuration] 2003-12-29
        if (base == null)
        {
            return null;
        }

        // on the first call initialize priorVariables
        // and add base as the first element
        if (priorVariables == null)
        {
            priorVariables = new ArrayList<>();
            priorVariables.add(base);
        }

        int begin = -1;
        int end = -1;
        int prec = 0 - END_TOKEN.length();
        String variable = null;
        StringBuilder result = new StringBuilder();

        // FIXME: we should probably allow the escaping of the start token
        while (((begin = base.indexOf(START_TOKEN, prec + END_TOKEN.length())) > -1)
               && ((end = base.indexOf(END_TOKEN, begin)) > -1))
        {
            result.append(base.substring(prec + END_TOKEN.length(), begin));
            variable = base.substring(begin + START_TOKEN.length(), end);

            // if we've got a loop, create a useful exception message and throw
            if (priorVariables.contains(variable))
            {
                String initialBase = priorVariables.remove(0).toString();
                priorVariables.add(variable);
                StringBuilder priorVariableSb = new StringBuilder();

                // create a nice trace of interpolated variables like so:
                // var1->var2->var3
                for (Iterator it = priorVariables.iterator(); it.hasNext();)
                {
                    priorVariableSb.append(it.next());
                    if (it.hasNext())
                    {
                        priorVariableSb.append("->");
                    }
                }

                throw new IllegalStateException(
                    "infinite loop in property interpolation of " + initialBase + ": " + priorVariableSb.toString());
            }
            // otherwise, add this variable to the interpolation list.
            else
            {

                priorVariables.add(variable);
            }

            //QUESTION: getProperty or getPropertyDirect
            Object value = getProperty(variable);
            if (value != null)
            {
                result.append(interpolateHelper(value.toString(), priorVariables));

                // pop the interpolated variable off the stack
                // this maintains priorVariables correctness for
                // properties with multiple interpolations, e.g.
                // prop.name=${some.other.prop1}/blahblah/${some.other.prop2}
                priorVariables.remove(priorVariables.size() - 1);
            }
            else if (defaults != null && defaults.getString(variable, null) != null)
            {
                result.append(defaults.getString(variable));
            }
            else
            {

                //variable not defined - so put it back in the value
                result.append(START_TOKEN).append(variable).append(END_TOKEN);
            }
            prec = end;
        }
        result.append(base.substring(prec + END_TOKEN.length(), base.length()));

        return result.toString();
    }

    /**
     * Inserts a backslash before every comma and backslash.
     */
    private static String escape(String s)
    {
        StringBuilder buf = new StringBuilder(s);
        for (int i = 0; i < buf.length(); i++)
        {
            char c = buf.charAt(i);
            if (c == ',' || c == '\\')
            {
                buf.insert(i, '\\');
                i++;
            }
        }
        return buf.toString();
    }

    /**
     * Removes a backslash from every pair of backslashes.
     */
    private static String unescape(String s)
    {
        StringBuilder buf = new StringBuilder(s);
        for (int i = 0; i < buf.length() - 1; i++)
        {
            char c1 = buf.charAt(i);
            char c2 = buf.charAt(i + 1);
            if (c1 == '\\' && c2 == '\\')
            {
                buf.deleteCharAt(i);
            }
        }
        return buf.toString();
    }

    /**
     * Counts the number of successive times 'ch' appears in the
     * 'line' before the position indicated by the 'index'.
     */
    private static int countPreceding(String line, int index, char ch)
    {
        int i;
        for (i = index - 1; i >= 0; i--)
        {
            if (line.charAt(i) != ch)
            {
                break;
            }
        }
        return index - 1 - i;
    }

    /**
     * Checks if the line ends with odd number of backslashes
     */
    private static boolean endsWithSlash(String line)
    {
        if (!line.endsWith("\\"))
        {
            return false;
        }
        return (countPreceding(line, line.length() - 1, '\\') % 2 == 0);
    }

    /**
     * This class is used to read properties lines.  These lines do
     * not terminate with new-line chars but rather when there is no
     * backslash sign a the end of the line.  This is used to
     * concatenate multiple lines for readability.
     */
    static class PropertiesReader extends LineNumberReader
    {
        /**
         * Constructor.
         *
         * @param reader A Reader.
         */
        public PropertiesReader(Reader reader)
        {
            super(reader);
        }

        /**
         * Read a property.
         *
         * @return a String property
         * @throws IOException if there is difficulty reading the source.
         */
        public String readProperty() throws IOException
        {
            StringBuilder buffer = new StringBuilder();
            String line = readLine();
            while (line != null)
            {
                line = line.trim();
                if ((line.length() != 0) && (line.charAt(0) != '#'))
                {
                    if (endsWithSlash(line))
                    {
                        line = line.substring(0, line.length() - 1);
                        buffer.append(line);
                    }
                    else
                    {

                        buffer.append(line);
                        return buffer.toString();  // normal method end
                    }
                }
                line = readLine();
            }
            return null;  // EOF reached
        }
    }

    /**
     * This class divides into tokens a property value.  Token
     * separator is "," but commas into the property value are escaped
     * using the backslash in front.
     */
    static class PropertiesTokenizer extends StringTokenizer
    {
        /**
         * The property delimiter used while parsing (a comma).
         */
        static final String DELIMITER = ",";

        /**
         * Constructor.
         *
         * @param string A String.
         */
        public PropertiesTokenizer(String string)
        {
            super(string, DELIMITER);
        }

        /**
         * Check whether the object has more tokens.
         *
         * @return True if the object has more tokens.
         */
        @Override
        public boolean hasMoreTokens()
        {
            return super.hasMoreTokens();
        }

        /**
         * Get next token.
         *
         * @return A String.
         */
        @Override
        public String nextToken()
        {
            StringBuilder buffer = new StringBuilder();

            while (hasMoreTokens())
            {
                String token = super.nextToken();
                if (endsWithSlash(token))
                {
                    buffer.append(token.substring(0, token.length() - 1));
                    buffer.append(DELIMITER);
                }
                else
                {

                    buffer.append(token);
                    break;
                }
            }

            return buffer.toString().trim();
        }
    }

    /**
     * Creates an empty extended properties object.
     */
    public ExtProperties()
    {
        super();
    }

    /**
     * Creates and loads the extended properties from the specified file.
     *
     * @param file  the filename to load
     * @throws IOException if a file error occurs
     */
    public ExtProperties(String file) throws IOException
    {
        this(file, null);
    }

    /**
     * Creates and loads the extended properties from the specified file.
     *
     * @param file  the filename to load
     * @param defaultFile  a second filename to load default values from
     * @throws IOException if a file error occurs
     */
    public ExtProperties(String file, String defaultFile) throws IOException
    {
        this.file = file;

        basePath = new File(file).getAbsolutePath();
        basePath = basePath.substring(0, basePath.lastIndexOf(fileSeparator) + 1);

        FileInputStream in = null;
        try
        {
            in = new FileInputStream(file);
            this.load(in);
        }
        finally
        {
            try
            {
                if (in != null)
                {
                    in.close();
                }
            }
            catch (IOException ex) {}
        }

        if (defaultFile != null)
        {
            defaults = new ExtProperties(defaultFile);
        }
    }

    /**
     * Indicate to client code whether property
     * resources have been initialized or not.
     * @return initialization status
     */
    public boolean isInitialized()
    {
        return isInitialized;
    }

    /**
     * Gets the property value for including other properties files.
     * By default it is "include".
     *
     * @return A String.
     */
    public String getInclude()
    {
        return include;
    }

    /**
     * Sets the property value for including other properties files.
     * By default it is "include".
     *
     * @param inc A String.
     */
    public void setInclude(String inc)
    {
        include = inc;
    }

    /**
     * Load the properties from the given input stream.
     *
     * @param input  the InputStream to load from
     * @throws IOException if an IO error occurs
     */
    public void load(InputStream input) throws IOException
    {
        load(input, null);
    }

    /**
     * Load the properties from the given input stream
     * and using the specified encoding.
     *
     * @param input  the InputStream to load from
     * @param enc  the encoding to use
     * @throws IOException if an IO error occurs
     */
    public synchronized void load(InputStream input, String enc) throws IOException
    {
        PropertiesReader reader = null;
        if (enc != null)
        {
            try
            {
                reader = new PropertiesReader(new InputStreamReader(input, enc));

            }
            catch (UnsupportedEncodingException ex)
            {
                // Another try coming up....
            }
        }

        // fall back to UTF-8
        if (reader == null)
        {
            reader = new PropertiesReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        }

        try
        {
            while (true)
            {
                String line = reader.readProperty();
                if (line == null)
                {
                    return;  // EOF
                }
                int equalSign = line.indexOf('=');

                if (equalSign > 0)
                {
                    String key = line.substring(0, equalSign).trim();
                    String value = line.substring(equalSign + 1).trim();

                    // Configure produces lines like this ... just ignore them
                    if ("".equals(value))
                    {
                        continue;
                    }

                    if (getInclude() != null && key.equalsIgnoreCase(getInclude()))
                    {
                        // Recursively load properties files.
                        File file = null;

                        if (value.startsWith(fileSeparator))
                        {
                            // We have an absolute path so we'll use this
                            file = new File(value);

                        }
                        else
                        {

                            // We have a relative path, and we have two
                            // possible forms here. If we have the "./" form
                            // then just strip that off first before continuing.
                            if (value.startsWith("." + fileSeparator))
                            {
                                value = value.substring(2);
                            }

                            file = new File(basePath + value);
                        }

                        if (file.exists() && file.canRead())
                        {
                            load(new FileInputStream(file));
                        }
                    }
                    else
                    {

                        addProperty(key, value);
                    }
                }
            }
        }
        finally
        {
            // Loading is initializing
            isInitialized = true;
        }
    }

    /**
     * Gets a property from the configuration.
     *
     * @param key property to retrieve
     * @return value as object. Will return user value if exists,
     *        if not then default value if exists, otherwise null
     */
    public Object getProperty(String key)
    {
        // first, try to get from the 'user value' store
        Object obj = this.get(key);

        if (obj == null)
        {
            // if there isn't a value there, get it from the
            // defaults if we have them
            if (defaults != null)
            {
                obj = defaults.get(key);
            }
        }

        return obj;
    }

    /**
     * Add a property to the configuration. If it already
     * exists then the value stated here will be added
     * to the configuration entry. For example, if
     *
     * <code>resource.loaders = file</code>
     *
     * is already present in the configuration and you
     *
     * <code>addProperty("resource.loaders", "classpath")</code>
     *
     * Then you will end up with a Vector like the
     * following:
     *
     * <code>["file", "classpath"]</code>
     *
     * @param key  the key to add
     * @param value  the value to add
     */
    public void addProperty(String key, Object value)
    {
        if (value instanceof String)
        {
            String str = (String) value;
            if (str.indexOf(PropertiesTokenizer.DELIMITER) > 0)
            {
                // token contains commas, so must be split apart then added
                PropertiesTokenizer tokenizer = new PropertiesTokenizer(str);
                while (tokenizer.hasMoreTokens())
                {
                    String token = tokenizer.nextToken();
                    addPropertyInternal(key, unescape(token));
                }
            }
            else
            {

                // token contains no commas, so can be simply added
                addPropertyInternal(key, unescape(str));
            }
        }
        else
        {

            addPropertyInternal(key, value);
        }

        // Adding a property connotes initialization
        isInitialized = true;
    }

    /**
     * Adds a key/value pair to the map.  This routine does
     * no magic morphing.  It ensures the keylist is maintained
     *
     * @param key  the key to store at
     * @param value  the decoded object to store
     */
    private void addPropertyDirect(String key, Object value)
    {
        // safety check
        if (!containsKey(key))
        {
            keysAsListed.add(translateKey(key));
        }
        put(key, value);
    }

    /**
     * Adds a decoded property to the map w/o checking for commas - used
     * internally when a property has been broken up into
     * strings that could contain escaped commas to prevent
     * the inadvertent vectorization.
     * <p>
     * Thanks to Leon Messerschmidt for this one.
     *
     * @param key  the key to store at
     * @param value  the decoded object to store
     */
    private void addPropertyInternal(String key, Object value)
    {
        Object current = this.get(key);

        if (current instanceof String)
        {
            // one object already in map - convert it to a vector
            List<Object> values = new Vector<>(2);
            values.add(current);
            values.add(value);
            put(key, values);

        }
        else if (current instanceof List)
        {
            // already a list - just add the new token
            ((List) current).add(value);

        }
        else
        {

            // brand new key - store in keysAsListed to retain order
            if (!containsKey(key))
            {
                keysAsListed.add(translateKey(key));
            }
            put(key, value);
        }
    }

    /**
     * Set a property, this will replace any previously
     * set values. Set values is implicitly a call
     * to clearProperty(key), addProperty(key,value).
     *
     * @param key  the key to set
     * @param value  the value to set
     */
    public void setProperty(String key, Object value)
    {
        clearProperty(key);
        addProperty(key, value);
    }

    /**
     * Save the properties to the given output stream.
     * <p>
     * The stream is not closed, but it is flushed.
     *
     * @param output  an OutputStream, may be null
     * @param header  a textual comment to act as a file header
     * @throws IOException if an IO error occurs
     */
    public synchronized void save(OutputStream output, String header) throws IOException
    {
        if (output == null)
        {
            return;
        }
        PrintWriter theWrtr = new PrintWriter(output);
        if (header != null)
        {
            theWrtr.println(header);
        }

        Enumeration theKeys = keys();
        while (theKeys.hasMoreElements())
        {
            String key = (String) theKeys.nextElement();
            Object value = get(key);
            if (value != null)
            {
                if (value instanceof String)
                {
                    StringBuilder currentOutput = new StringBuilder();
                    currentOutput.append(key);
                    currentOutput.append("=");
                    currentOutput.append(escape((String) value));
                    theWrtr.println(currentOutput.toString());

                }
                else if (value instanceof List)
                {
                    List<String> values = (List<String>) value;
                    for (String currentElement : values)
                    {
                        StringBuilder currentOutput = new StringBuilder();
                        currentOutput.append(key);
                        currentOutput.append("=");
                        currentOutput.append(escape(currentElement));
                        theWrtr.println(currentOutput.toString());
                    }
                }
            }
            theWrtr.println();
            theWrtr.flush();
        }
    }

    /**
     * Combines an existing Hashtable with this Hashtable.
     * <p>
     * Warning: It will overwrite previous entries without warning.
     *
     * @param props  the properties to combine
     */
    public void combine(ExtProperties props)
    {
        for (Iterator it = props.getKeys(); it.hasNext();)
        {
            String key = (String) it.next();
            setProperty(key, props.get(key));
        }
    }

    /**
     * Clear a property in the configuration.
     *
     * @param key  the property key to remove along with corresponding value
     */
    public void clearProperty(String key)
    {
        key = translateKey(key);
        if (containsKey(key))
        {
            // we also need to rebuild the keysAsListed or else
            // things get *very* confusing
            for (int i = 0; i < keysAsListed.size(); i++)
            {
                if (( keysAsListed.get(i)).equals(key))
                {
                    keysAsListed.remove(i);
                    break;
                }
            }
            remove(key);
        }
    }

    /**
     * Get the list of the keys contained in the configuration
     * repository.
     *
     * @return an Iterator over the keys
     */
    public Iterator<String> getKeys()
    {
        return keysAsListed.iterator();
    }

    /**
     * Get the list of the keys contained in the configuration
     * repository that match the specified prefix.
     *
     * @param prefix  the prefix to match
     * @return an Iterator of keys that match the prefix
     */
    public Iterator<String> getKeys(String prefix)
    {
        Iterator<String> keys = getKeys();
        ArrayList<String> matchingKeys = new ArrayList<>();

        while (keys.hasNext())
        {
            String key = keys.next();

            if (key.startsWith(prefix))
            {
                matchingKeys.add(key);
            }
        }
        return matchingKeys.iterator();
    }

    /**
     * Create an ExtProperties object that is a subset
     * of this one. Take into account duplicate keys
     * by using the setProperty() in ExtProperties.
     *
     * @param prefix  the prefix to get a subset for
     * @return a new independent ExtProperties
     */
    public ExtProperties subset(String prefix)
    {
        ExtProperties c = new ExtProperties();
        Iterator<String> keys = getKeys();
        boolean validSubset = false;

        while (keys.hasNext())
        {
            String key = keys.next();

            if (key.startsWith(prefix))
            {
                if (!validSubset)
                {
                    validSubset = true;
                }

                /*
                 * Check to make sure that c.subset(prefix) doesn't
                 * blow up when there is only a single property
                 * with the key prefix. This is not a useful
                 * subset but it is a valid subset.
                 */
                String newKey = null;
                if (key.length() == prefix.length())
                {
                    newKey = prefix;
                }
                else
                {

                    newKey = key.substring(prefix.length() + 1);
                }

                /*
                 *  use addPropertyDirect() - this will plug the data as
                 *  is into the Map, but will also do the right thing
                 *  re key accounting
                 */
                c.addPropertyDirect(newKey, get(key));
            }
        }

        if (validSubset)
        {
            return c;
        }
        else
        {

            return null;
        }
    }

    /**
     * Display the configuration for debugging purposes to System.out.
     */
    public void display()
    {
        Iterator<String> i = getKeys();

        while (i.hasNext())
        {
            String key = i.next();
            Object value = get(key);
            System.out.println(key + " => " + value);
        }
    }

    /**
     * Get a string associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated string.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a String.
     */
    public String getString(String key)
    {
        return getString(key, null);
    }

    /**
     * Get a string associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated string if key is found,
     * default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a String.
     */
    public String getString(String key, String defaultValue)
    {
        Object value = get(key);

        if (value instanceof String)
        {
            return interpolate((String) value);

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return interpolate(defaults.getString(key, defaultValue));
            }
            else
            {

                return interpolate(defaultValue);
            }
        }
        else if (value instanceof List)
        {
            return interpolate((String) ((List) value).get(0));
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a String object");
        }
    }

    /**
     * Get a list of properties associated with the given
     * configuration key.
     *
     * @param key The configuration key.
     * @return The associated properties if key is found.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a String/List.
     * @throws IllegalArgumentException if one of the tokens is
     * malformed (does not contain an equals sign).
     */
    public Properties getProperties(String key)
    {
        return getProperties(key, new Properties());
    }

    /**
     * Get a list of properties associated with the given
     * configuration key.
     *
     * @param key The configuration key.
     * @param defaults default values
     * @return The associated properties if key is found.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a String/List.
     * @throws IllegalArgumentException if one of the tokens is
     * malformed (does not contain an equals sign).
     */
    public Properties getProperties(String key, Properties defaults)
    {
        /*
         * Grab an array of the tokens for this key.
         */
        String[] tokens = getStringArray(key);

        // Each token is of the form 'key=value'.
        Properties props = new Properties(defaults);
        for (String token : tokens)
        {
            int equalSign = token.indexOf('=');
            if (equalSign > 0)
            {
                String pkey = token.substring(0, equalSign).trim();
                String pvalue = token.substring(equalSign + 1).trim();
                props.put(pkey, pvalue);
            }
            else
            {
                throw new IllegalArgumentException('\'' + token + "' does not contain an equals sign");
            }
        }
        return props;
    }

    /**
     * Get an array of strings associated with the given configuration
     * key.
     *
     * @param key The configuration key.
     * @return The associated string array if key is found.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a String/List.
     */
    public String[] getStringArray(String key)
    {
        Object value = get(key);

        List<Object> values;
        if (value instanceof String)
        {
            values = new Vector<>(1);
            values.add(value);

        }
        else if (value instanceof List)
        {
            values = (List) value;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getStringArray(key);
            }
            else
            {

                return new String[0];
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a String/List object");
        }

        String[] tokens = new String[values.size()];
        for (int i = 0; i < tokens.length; i++)
        {
            tokens[i] = (String) values.get(i);
        }

        return tokens;
    }

    /**
     * Get a Vector of strings associated with the given configuration
     * key.
     *
     * @param key The configuration key.
     * @return The associated Vector.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Vector.
     */
    public Vector getVector(String key)
    {
        return getVector(key, null);
    }

    /**
     * Get a Vector of strings associated with the given configuration key.
     * <p>
     * The list is a copy of the internal data of this object, and as
     * such you may alter it freely.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated Vector.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Vector.
     */
    public Vector getVector(String key, Vector defaultValue)
    {
        Object value = get(key);

        if (value instanceof List)
        {
            return new Vector<Object>((List) value);

        }
        else if (value instanceof String)
        {
            Vector<Object> values = new Vector<>(1);
            values.add(value);
            put(key, values);
            return values;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getVector(key, defaultValue);
            }
            else
            {

                return ((defaultValue == null) ? new Vector<>() : defaultValue);
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Vector object");
        }
    }

    /**
     * Get a List of strings associated with the given configuration key.
     * <p>
     * The list is a copy of the internal data of this object, and as
     * such you may alter it freely.
     *
     * @param key The configuration key.
     * @return The associated List object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a List.
     * @since Commons Collections 3.2
     */
    public List getList(String key)
    {
        return getList(key, null);
    }

    /**
     * Get a List of strings associated with the given configuration key.
     * <p>
     * The list is a copy of the internal data of this object, and as
     * such you may alter it freely.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated List.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a List.
     * @since Commons Collections 3.2
     */
    public List getList(String key, List defaultValue)
    {
        Object value = get(key);

        if (value instanceof List)
        {
            return new ArrayList((List) value);

        }
        else if (value instanceof String)
        {
            List values = new ArrayList(1);
            values.add(value);
            put(key, values);
            return values;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getList(key, defaultValue);
            }
            else
            {

                return ((defaultValue == null) ? new ArrayList() : defaultValue);
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a List object");
        }
    }

    /**
     * Get a boolean associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated boolean.
     * @throws NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Boolean.
     */
    public boolean getBoolean(String key)
    {
        Boolean b = getBoolean(key, null);
        if (b != null)
        {
            return b;
        }
        else
        {

            throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a boolean associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated boolean.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Boolean.
     */
    public boolean getBoolean(String key, boolean defaultValue)
    {
        return getBoolean(key, Boolean.valueOf(defaultValue));
    }

    /**
     * Get a boolean associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated boolean if key is found and has valid
     * format, default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Boolean.
     */
    public Boolean getBoolean(String key, Boolean defaultValue)
    {

        Object value = get(key);

        if (value instanceof Boolean)
        {
            return (Boolean) value;

        }
        else if (value instanceof String)
        {
            String s = testBoolean(((String) value).trim());
            Boolean b = Boolean.valueOf(s);
            put(key, b);
            return b;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getBoolean(key, defaultValue);
            }
            else
            {

                return defaultValue;
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Boolean object");
        }
    }

    /**
     * Test whether the string represent by value maps to a boolean
     * value or not. We will allow <code>true</code>, <code>on</code>,
     * and <code>yes</code> for a <code>true</code> boolean value, and
     * <code>false</code>, <code>off</code>, and <code>no</code> for
     * <code>false</code> boolean values.  Case of value to test for
     * boolean status is ignored.
     *
     * @param value  the value to test for boolean state
     * @return <code>true</code> or <code>false</code> if the supplied
     * text maps to a boolean value, or <code>null</code> otherwise.
     */
    public String testBoolean(String value)
    {
        String s = value.toLowerCase(Locale.ROOT);

        switch (s)
        {
            case "true":
            case "on":
            case "yes":
                return "true";
            case "false":
            case "off":
            case "no":
                return "false";
            default:
                return null;
        }
    }

    /**
     * Get a byte associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated byte.
     * @throws NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Byte.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public byte getByte(String key)
    {
        Byte b = getByte(key, null);
        if (b != null)
        {
            return b;
        }
        else
        {

            throw new NoSuchElementException('\'' + key + " doesn't map to an existing object");
        }
    }

    /**
     * Get a byte associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated byte.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Byte.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public byte getByte(String key, byte defaultValue)
    {
        return getByte(key, Byte.valueOf(defaultValue));
    }

    /**
     * Get a byte associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated byte if key is found and has valid
     * format, default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Byte.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Byte getByte(String key, Byte defaultValue)
    {
        Object value = get(key);

        if (value instanceof Byte)
        {
            return (Byte) value;

        }
        else if (value instanceof String)
        {
            Byte b = Byte.valueOf((String) value);
            put(key, b);
            return b;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getByte(key, defaultValue);
            }
            else
            {

                return defaultValue;
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Byte object");
        }
    }

    /**
     * Get a short associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated short.
     * @throws NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Short.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public short getShort(String key)
    {
        Short s = getShort(key, null);
        if (s != null)
        {
            return s;
        }
        else
        {

            throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a short associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated short.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Short.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public short getShort(String key, short defaultValue)
    {
        return getShort(key, Short.valueOf(defaultValue));
    }

    /**
     * Get a short associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated short if key is found and has valid
     * format, default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Short.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Short getShort(String key, Short defaultValue)
    {
        Object value = get(key);

        if (value instanceof Short)
        {
            return (Short) value;

        }
        else if (value instanceof String)
        {
            Short s = Short.valueOf((String) value);
            put(key, s);
            return s;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getShort(key, defaultValue);
            }
            else
            {

                return defaultValue;
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Short object");
        }
    }

    /**
     * The purpose of this method is to get the configuration resource
     * with the given name as an integer.
     *
     * @param name The resource name.
     * @return The value of the resource as an integer.
     */
    public int getInt(String name)
    {
        return getInteger(name);
    }

    /**
     * The purpose of this method is to get the configuration resource
     * with the given name as an integer, or a default value.
     *
     * @param name The resource name
     * @param def The default value of the resource.
     * @return The value of the resource as an integer.
     */
    public int getInt(String name, int def)
    {
        return getInteger(name, def);
    }

    /**
     * Get a int associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated int.
     * @throws NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Integer.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public int getInteger(String key)
    {
        Integer i = getInteger(key, null);
        if (i != null)
        {
            return i;
        }
        else
        {

            throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a int associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated int.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Integer.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public int getInteger(String key, int defaultValue)
    {
        Integer i = getInteger(key, null);

        if (i == null)
        {
            return defaultValue;
        }
        return i;
    }

    /**
     * Get a int associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated int if key is found and has valid
     * format, default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Integer.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Integer getInteger(String key, Integer defaultValue)
    {
        Object value = get(key);

        if (value instanceof Integer)
        {
            return (Integer) value;

        }
        else if (value instanceof Number)
        {
            return ((Number)value).intValue();
        }
        else if (value instanceof String)
        {
            Integer i = Integer.valueOf((String) value);
            put(key, i);
            return i;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getInteger(key, defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Integer object");
        }
    }

    /**
     * Get a long associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated long.
     * @throws NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Long.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public long getLong(String key)
    {
        Long l = getLong(key, null);
        if (l != null)
        {
            return l;
        }
        else
        {

            throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a long associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated long.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Long.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public long getLong(String key, long defaultValue)
    {
        return getLong(key, Long.valueOf(defaultValue));
    }

    /**
     * Get a long associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated long if key is found and has valid
     * format, default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Long.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Long getLong(String key, Long defaultValue)
    {
        Object value = get(key);

        if (value instanceof Long)
        {
            return (Long) value;
        }
        else if (value instanceof Number)
        {
            return ((Number)value).longValue();
        }
        else if (value instanceof String)
        {
            Long l = Long.valueOf((String) value);
            put(key, l);
            return l;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getLong(key, defaultValue);
            }
            else
            {

                return defaultValue;
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Long object");
        }
    }

    /**
     * Get a float associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated float.
     * @throws NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Float.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public float getFloat(String key)
    {
        Float f = getFloat(key, null);
        if (f != null)
        {
            return f;
        }
        else
        {

            throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a float associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated float.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Float.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public float getFloat(String key, float defaultValue)
    {
        return getFloat(key, Float.valueOf(defaultValue));
    }

    /**
     * Get a float associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated float if key is found and has valid
     * format, default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Float.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Float getFloat(String key, Float defaultValue)
    {
        Object value = get(key);

        if (value instanceof Float)
        {
            return (Float) value;

        }
        else if (value instanceof Number)
        {
            return ((Number)value).floatValue();
        }
        else if (value instanceof String)
        {
            Float f = Float.valueOf((String) value);
            put(key, f);
            return f;

        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getFloat(key, defaultValue);
            }
            else
            {

                return defaultValue;
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Float object");
        }
    }

    /**
     * Get a double associated with the given configuration key.
     *
     * @param key The configuration key.
     * @return The associated double.
     * @throws NoSuchElementException is thrown if the key doesn't
     * map to an existing object.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Double.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public double getDouble(String key)
    {
        Double d = getDouble(key, null);
        if (d != null)
        {
            return d;
        }
        else
        {

            throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
        }
    }

    /**
     * Get a double associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated double.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Double.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public double getDouble(String key, double defaultValue)
    {
        return getDouble(key, Double.valueOf(defaultValue));
    }

    /**
     * Get a double associated with the given configuration key.
     *
     * @param key The configuration key.
     * @param defaultValue The default value.
     * @return The associated double if key is found and has valid
     * format, default value otherwise.
     * @throws ClassCastException is thrown if the key maps to an
     * object that is not a Double.
     * @throws NumberFormatException is thrown if the value mapped
     * by the key has not a valid number format.
     */
    public Double getDouble(String key, Double defaultValue)
    {
        Object value = get(key);

        if (value instanceof Double)
        {
            return (Double) value;
        }
        else if (value instanceof Number)
        {
            return ((Number)value).doubleValue();
        }
        else if (value instanceof String)
        {
            Double d = Double.valueOf((String) value);
            put(key, d);
            return d;
        }
        else if (value == null)
        {
            if (defaults != null)
            {
                return defaults.getDouble(key, defaultValue);
            }
            else
            {

                return defaultValue;
            }
        }
        else
        {

            throw new ClassCastException('\'' + key + "' doesn't map to a Double object");
        }
    }

    /**
     * Convert a standard properties class into a configuration class.
     * <p>
     * NOTE: From Commons Collections 3.2 this method will pick up
     * any default parent Properties of the specified input object.
     *
     * @param props  the properties object to convert
     * @return new ExtProperties created from props
     */
    public static ExtProperties convertProperties(Properties props)
    {
        ExtProperties c = new ExtProperties();

        for (Enumeration e = props.propertyNames(); e.hasMoreElements();)
        {
            String s = (String) e.nextElement();
            /*
             * We use get() and not getProperty() in case the user didn't
             * respect the Properties contract of only holding strings.
             * Sun's fault.
             */
            c.setProperty(s, props.get(s));
        }

        return c;
    }

    /**
     * Convert a Map into a configuration class.
     * <p>
     * NOTE: From Commons Collections 3.2 this method will pick up
     * any default parent Properties of the specified input object.
     *
     * @param props  the Map object to convert
     * @return new ExtProperties created from props
     */
    public static ExtProperties convertProperties(Map props)
    {
        ExtProperties c = new ExtProperties();

        for (Map.Entry entry : (Set<Map.Entry>)props.entrySet())
        {
            c.setProperty(String.valueOf(entry.getKey()), entry.getValue());
        }
        return c;
    }
}
