com.norconex.commons.lang.map.Properties.java Source code

Java tutorial

Introduction

Here is the source code for com.norconex.commons.lang.map.Properties.java

Source

/* Copyright 2010-2014 Norconex Inc.
 *
 * Licensed 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 com.norconex.commons.lang.map;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

/**
 * <p>This class is a enhanced version of {@link java.util.Properties}
 * that enforces the use of String keys and values internally, but offers many
 * convenience methods for storing and retrieving multiple values of different
 * types (e.g. Integer, Locale, File, etc). You can also see it as a 
 * string-based multi-value map with helpful methods. While it does not extend 
 * {@link java.util.Properties}, it offers similar load and store
 * methods and can be used as a replacement for it in many cases.</p>
 * 
 * <p>As of <b>1.4</b>, this class no longer extends {@code TreeMap}.
 * It now extends {@link ObservableMap} which means you can listen
 * for map changes.</p>
 * 
 * <p>To insert values, there are <i>set</i> methods and <i>add</i> methods.
 * The <i>set</i> methods will replace any value(s) already present under the 
 * given key.  It is essentially the same behavior as 
 * {@link Map#put(Object, Object)}.  The <i>add</i> method will add the 
 * new value(s) to the list of already existing ones (if any).
 * </p>
 * 
 * <p>Upon encountering a problem in parsing the
 * data to its target format, a {@link PropertiesException} is thrown.</p>
 * @author Pascal Essiembre
 */
@SuppressWarnings("nls")
public class Properties extends ObservableMap<String, List<String>> implements Serializable {

    private static final long serialVersionUID = -7215126924574341L;
    private static final Logger LOG = LogManager.getLogger(Properties.class);

    /**
     * Default delimiter when storing/loading multi-values to/from 
     * <code>.properties</code> files.
     */
    public static final String DEFAULT_MULTIVALUE_DELIMITER = "^|~";

    private final boolean caseSensitiveKeys;
    private String multiValueDelimiter = DEFAULT_MULTIVALUE_DELIMITER;

    /**
     * Create a new instance with case-sensitive keys.
     * Internally wraps a {@link HashMap} to store keys and values.
     */
    public Properties() {
        this(false);
    }

    /**
     * Creates a new instance. Internally wraps a {@link HashMap} to 
     * store keys and values.
     * @param caseSensitiveKeys methods taking a key argument 
     *        will consider the key being passed without consideration
     *        for character case.
     */
    public Properties(boolean caseSensitiveKeys) {
        this(null, caseSensitiveKeys);
    }

    /**
     * Decorates {@code Map} as a {@code Properties}.
     * As of version <b>1.4</b> the {@code Map} argument is decorated so that
     * modifications to this instance will also modify the supplied {@code Map}.
     * To use a {@code Map} to initialize values only, use the 
     * {@link #load(Map)} method.
     * @param map the Map to decorate 
     */
    public Properties(Map<String, List<String>> map) {
        this(map, false);
    }

    /**
     * Decorates a {@code Map} argument as a {@code Properties}.
     * As of version <b>1.4</b> the {@code Map} argument is decorated so that
     * modifications to this instance will also modify the supplied {@code Map}.
     * To use a {@code Map} to initialize values only, use the 
     * {@link #load(Map)} method.
     * @param map the Map to decorate 
     * @param caseSensitiveKeys methods taking a key argument 
     *        will consider the key being passed without consideration
     *        for character case.
     */
    public Properties(Map<String, List<String>> map, boolean caseSensitiveKeys) {
        super(map);
        this.caseSensitiveKeys = caseSensitiveKeys;
    }

    /**
     * Gets whether keys are case sensitive or not.
     * @return <code>true</code> if case sensitive
     * @since 1.4
     */
    public boolean isCaseSensitiveKeys() {
        return caseSensitiveKeys;
    }

    /**
     * Gets multiple value string delimiter.
     * @return multiple value string delimiter
     * @since 1.4
     */
    public String getMultiValueDelimiter() {
        return multiValueDelimiter;
    }

    /**
     * Sets multiple value string delimiter.
     * @param multiValueDelimiter multiple value string delimiter
     * @since 1.4
     */
    public void setMultiValueDelimiter(String multiValueDelimiter) {
        this.multiValueDelimiter = multiValueDelimiter;
    }

    //--- Store ----------------------------------------------------------------
    /**
     * Writes this property list (key and element pairs) in this
     * <code>Properties</code> table to the output stream as UTF-8 in a format 
     * suitable for loading into a <code>Properties</code> table using the
     * {@link #load(InputStream) load} method.
     * Otherwise, the same considerations as
     * {@link #store(OutputStream, String)} apply.
     * @param   comments   a description of the property list.
     * @return the properties as string
     * @throws IOException problem storing to string
     */
    public String storeToString(String comments) throws IOException {
        StringWriter writer = new StringWriter();
        store(writer, comments);
        String str = writer.toString();
        writer.close();
        return str;
    }

    /**
     * Writes this {@link Map} (key and element pairs) to the output character
     * stream in a format suitable for using the 
     * {@link #load(Reader)} method. 
     * If a key only has one value, then this method behavior is the
     * exact same as the {@link Properties#store(Writer, String)} method.
     * Keys with multi-values are joined into a single string, using
     * the default delimiter:
     * {@link Properties#DEFAULT_MULTIVALUE_DELIMITER}
     * @param   writer     an output character stream writer.
     * @param   comments   a description of the property list.
     * @throws IOException i/o problem
     * @see Properties#store(Writer, String)
     */
    public void store(Writer writer, String comments) throws IOException {
        store(writer, comments, DEFAULT_MULTIVALUE_DELIMITER);
    }

    /**
     * Writes this {@link Map} (key and element pairs) to the output character
     * stream in a format suitable for using the 
     * {@link #load(Reader, String)} method. 
     * If a key only has one value, then this method behavior is the
     * exact same as the {@link Properties#store(Writer, String)} method.
     * Keys with multi-values are joined into a single string, using
     * the delimiter provided.
     * @param   writer     an output character stream writer.
     * @param   comments   a description of the property list.
     * @param delimiter string to used as a separator when joining 
     *        multiple values for the same key.
     * @throws IOException i/o problem
     * @see Properties#store(Writer, String)
     */
    public void store(Writer writer, String comments, String delimiter) throws IOException {
        java.util.Properties p = new java.util.Properties();
        for (String key : keySet()) {
            List<String> values = getStrings(key);
            p.put(key, StringUtils.join(values, delimiter));
        }
        p.store(writer, comments);
        p = null;
    }

    /**
     * Writes this {@link Map} (key and element pairs) to the output character
     * stream in a format suitable for using the 
     * {@link #load(InputStream)} method. 
     * If a key only has one value, then this method behavior is the
     * exact same as the {@link Properties#store(OutputStream, String)} method.
     * Keys with multi-values are joined into a single string, using
     * the default delimiter:
     * {@link Properties#DEFAULT_MULTIVALUE_DELIMITER}
     * @param   out      an output stream.
     * @param   comments   a description of the property list.
     * @throws IOException i/o problem
     * @see Properties#store(OutputStream, String)
     */
    public void store(OutputStream out, String comments) throws IOException {
        store(out, comments, DEFAULT_MULTIVALUE_DELIMITER);
    }

    /**
     * Writes this {@link Map} (key and element pairs) to the output character
     * stream as UTF-8 in a format suitable for using the 
     * {@link #load(InputStream, String)} method. 
     * If a key only has one value, then this method behavior is the
     * exact same as the {@link Properties#store(OutputStream, String)} method.
     * Keys with multi-values are joined into a single string, using
     * the delimiter provided.
     * @param   out      an output stream.
     * @param   comments   a description of the property list.
     * @param delimiter delimiter string to used as a separator when joining 
     *        multiple values for the same key.
     * @throws IOException i/o problem
     * @see Properties#store(OutputStream, String)
     */
    public void store(OutputStream out, String comments, String delimiter) throws IOException {
        store(new OutputStreamWriter(out, CharEncoding.UTF_8), comments, delimiter);
    }

    /**
     * Emits an XML document representing all of the properties contained
     * in this {@link Map}, using the specified encoding.
     * If a key only has one value, then this method behavior is the
     * exact same as the 
     * {@link Properties#storeToXML(OutputStream, String, String)} method,
     * where the character encoding is "UTF-8".
     * Keys with multi-values are joined into a single string, using
     * the default delimiter:
     * {@link Properties#DEFAULT_MULTIVALUE_DELIMITER}
     * @param os the output stream on which to emit the XML document.
     * @param comment a description of the property list, or <code>null</code>
     *        if no comment is desired.
     * @throws IOException i/o problem
     * @see Properties#storeToXML(OutputStream, String, String)
     */
    public synchronized void storeToXML(OutputStream os, String comment) throws IOException {
        storeToXML(os, comment, CharEncoding.UTF_8);
    }

    /**
     * Emits an XML document representing all of the properties contained
     * in this {@link Map}, using the specified encoding.
     * If a key only has one value, then this method behavior is the
     * exact same as the 
     * {@link Properties#storeToXML(OutputStream, String, String)} method.
     * Keys with multi-values are joined into a single string, using
     * the default delimiter:
     * {@link Properties#DEFAULT_MULTIVALUE_DELIMITER}
     * @param os the output stream on which to emit the XML document.
     * @param comment a description of the property list, or <code>null</code>
     *        if no comment is desired.
     * @param encoding character encoding
     * @throws IOException i/o problem
     * @see Properties#storeToXML(OutputStream, String, String)
     */
    public synchronized void storeToXML(OutputStream os, String comment, String encoding) throws IOException {
        storeToXML(os, comment, encoding, DEFAULT_MULTIVALUE_DELIMITER);
    }

    /**
     * Emits an XML document representing all of the properties contained
     * in this {@link Map}, using the specified encoding.
     * If a key only has one value, then this method behavior is the
     * exact same as the 
     * {@link Properties#storeToXML(OutputStream, String, String)} method.
     * Keys with multi-values are joined into a single string, using
     * the delimiter provided.
     * @param os the output stream on which to emit the XML document.
     * @param comment a description of the property list, or <code>null</code>
     *        if no comment is desired.
     * @param encoding character encoding
     * @param delimiter delimiter string to used as a separator when joining 
     *        multiple values for the same key.
     * @throws IOException i/o problem
     * @see Properties#storeToXML(OutputStream, String, String)
     */
    public synchronized void storeToXML(OutputStream os, String comment, String encoding, String delimiter)
            throws IOException {
        java.util.Properties p = new java.util.Properties();
        for (String key : keySet()) {
            List<String> values = getStrings(key);
            p.put(key, StringUtils.join(values, delimiter));
        }
        p.storeToXML(os, comment, encoding);
        p = null;
    }

    //--- Load -----------------------------------------------------------------
    /**
     * Reads a property list (key and element pairs) from the input
     * character stream in a simple line-oriented format.
     * If a key was stored with multiple values using a delimiter, this,
     * method will split these values appropriately assuming the delimiter is
     * {@link Properties#DEFAULT_MULTIVALUE_DELIMITER}
     * If the key value was stored as a
     * single value, then this method behavior is the
     * exact same as the 
     * {@link Properties#load(Reader)} method.
     * @param   reader   the input character stream.
     * @throws IOException i/o problem
     * @see Properties#load(Reader)
     */
    public synchronized void load(Reader reader) throws IOException {
        load(reader, DEFAULT_MULTIVALUE_DELIMITER);
    }

    /**
     * Reads a property list (key and element pairs) from the input
     * character stream in a simple line-oriented format.
     * If a key was stored with multiple values using a delimiter, 
     * this method will split these values appropriately provided the 
     * supplied delimiter is the same. If the key value was stored as a
     * single value, then this method behavior is the
     * exact same as the 
     * {@link Properties#load(Reader)} method.
     * @param   reader   the input character stream.
     * @param delimiter delimiter string to used to parse a multi value
     *        key.
     * @throws IOException i/o problem
     * @see Properties#load(Reader)
     */
    public synchronized void load(Reader reader, String delimiter) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.load(reader);
        for (String key : p.stringPropertyNames()) {
            List<String> values = new ArrayList<String>();
            String value = p.getProperty(key);
            if (value != null) {
                values.addAll(Arrays.asList(StringUtils.splitByWholeSeparator(value, delimiter)));
            }
            put(key, values);
        }
        p = null;
    }

    /**
     * <p>Reads all key/value pairs in the given map, and 
     * add them to this <code>Map</code>.  Keys are converted to strings
     * using their toString() method, with exception
     * of values being arrays or collections.  In such case, the entry
     * is considered a multi-value one and each value will be converted
     * to individual strings. <code>null</code> keys are ignored.
     * <code>null</code> values are converted to an empty string.</p> 
     * <p>Changes to this instance
     * won't be reflected in the given <code>Map</code>.  If you want otherwise,
     * use invoke the constructor with a <code>Map</code> argument.</p>
     * 
     * @param map the map containing values to load
     */
    public synchronized void load(Map<?, ?> map) {
        if (map != null) {
            for (Entry<?, ?> entry : map.entrySet()) {
                Object keyObj = entry.getKey();
                if (keyObj == null) {
                    continue;
                }
                String key = Objects.toString(keyObj, null);
                Object valObj = entry.getValue();
                if (valObj == null) {
                    valObj = StringUtils.EMPTY;
                }
                Iterable<?> it = null;
                if (valObj.getClass().isArray()) {
                    it = Arrays.asList((Object[]) valObj);
                } else if (valObj instanceof Iterable) {
                    it = (Iterable<?>) valObj;
                }
                if (it == null) {
                    addString(key, Objects.toString(valObj, null));
                } else {
                    for (Object val : it) {
                        addString(key, Objects.toString(val, null));
                    }
                }
            }
        }
    }

    /**
     * Reads a property list (key and element pairs) from the input
     * character stream (UTF-8) in a simple line-oriented format.
     * If a key was stored with multiple values using a delimiter, this,
     * method will split these values appropriately assuming the delimiter is
     * {@link Properties#DEFAULT_MULTIVALUE_DELIMITER}
     * If the key value was stored as a
     * single value, then this method behavior is the
     * exact same as the 
     * {@link Properties#load(InputStream)} method.
     * @param   inStream   the input stream.
     * @throws IOException i/o problem
     * @see Properties#load(InputStream)
     */
    public synchronized void load(InputStream inStream) throws IOException {
        load(new InputStreamReader(inStream, CharEncoding.UTF_8), DEFAULT_MULTIVALUE_DELIMITER);
    }

    /**
     * Reads a property list (key and element pairs) from the input
     * character stream (UTF8) in a simple line-oriented format.
     * If a key was stored with multiple values using a delimiter, 
     * this method will split these values appropriately provided the 
     * supplied delimiter is the same. If the key value was stored as a
     * single value, then this method behavior is the
     * exact same as the 
     * {@link Properties#load(InputStream)} method.
     * @param   inStream   the input stream.
     * @param delimiter delimiter string to used to parse a multi value
     *        key.
     * @throws IOException i/o problem
     * @see Properties#load(InputStream)
     */
    public synchronized void load(InputStream inStream, String delimiter) throws IOException {
        load(new InputStreamReader(inStream, CharEncoding.UTF_8), delimiter);
    }

    /**
     * Loads all of the properties represented by the XML document on the
     * specified input stream into this instance.
     * If a key was stored with multiple values using a delimiter, 
     * method will split these values appropriately assuming the delimiter is
     * {@link Properties#DEFAULT_MULTIVALUE_DELIMITER}
     * If the key value was stored as a
     * single value, then this method behavior is the
     * exact same as the 
     * {@link Properties#loadFromXML(InputStream)} method.
     * @param in in the input stream from which to read the XML document.
     * @throws IOException i/o problem
     */
    public synchronized void loadFromXML(InputStream in) throws IOException {
        loadFromXML(in, DEFAULT_MULTIVALUE_DELIMITER);
    }

    /**
     * Loads all of the properties represented by the XML document on the
     * specified input stream into this instance.
     * If a key was stored with multiple values using a delimiter, 
     * this method will split these values appropriately provided the 
     * supplied delimiter is the same. If the key value was stored as a
     * single value, then this method behavior is the
     * exact same as the 
     * {@link Properties#loadFromXML(InputStream)} method.
     * @param in in the input stream from which to read the XML document.
     * @param delimiter delimiter string to used to parse a multi value
     *        key.
     * @throws IOException i/o problem
     */
    public synchronized void loadFromXML(InputStream in, String delimiter) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.loadFromXML(in);
        List<String> values = new ArrayList<String>();
        for (String key : p.stringPropertyNames()) {
            String value = p.getProperty(key);
            if (value != null) {
                values.addAll(Arrays.asList(StringUtils.splitByWholeSeparator(value, delimiter)));
            }
            put(key, values);
        }
        p = null;
    }

    /**
     * Reads a property list (key and element pairs) from the UTF-8 input
     * string.  Otherwise, the same considerations as
     * {@link #load(InputStream)} apply.
     * @param str the string to load
     * @throws IOException problem loading string
     */
    public void loadFromString(String str) throws IOException {
        InputStream is = new ByteArrayInputStream(str.getBytes(CharEncoding.UTF_8));
        load(is);
        is.close();
    }

    //--- String ---------------------------------------------------------------
    /**
     * Gets value as string.
     * @param key property key
     * @return the value
     */
    public final String getString(String key) {
        List<String> list = get(key);
        if (list != null && !list.isEmpty()) {
            return list.get(0);
        }
        return null;
    }

    /**
     * Gets value as string.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final String getString(String key, String defaultValue) {
        String s = getString(key);
        if (s == null) {
            return defaultValue;
        }
        return s;
    }

    /**
     * Gets values as a list of strings.
     * @param key property key
     * @return the values
     */
    public final List<String> getStrings(String key) {
        List<String> values = get(key);
        if (values == null) {
            return new ArrayList<String>();
        }
        return new ArrayList<String>(values);
    }

    /**
     * Sets one or multiple string values replacing existing ones.  
     * Setting a string with a <code>null</code> value will set a blank string.
     * @param key the key of the value to set
     * @param values the values to set
     */
    public final void setString(String key, String... values) {
        List<String> list = new ArrayList<String>(values.length);
        for (String value : values) {
            if (value == null) {
                list.add("");
            } else {
                list.add(value);
            }
        }
        put(key, list);
    }

    /**
     * Adds one or multiple string values.  
     * Adding a string with a <code>null</code> value will set a blank string.
     * @param key the key of the value to set
     * @param values the values to set
     */
    public final void addString(String key, String... values) {
        List<String> list = get(key);
        if (list == null) {
            list = new ArrayList<String>(values.length);
        }
        for (String value : values) {
            if (value == null) {
                list.add("");
            } else {
                list.add(value);
            }
        }
        put(key, list);
    }

    //--- Integer --------------------------------------------------------------
    /**
     * Gets value as an integer.
     * @param key property key
     * @return the value
     */
    public final int getInt(String key) {
        try {
            return Integer.parseInt(getString(key));
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse integer value.", key, getString(key), e);
        }
    }

    /**
     * Gets value as an integer.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final int getInt(String key, int defaultValue) {
        String value = getString(key, "" + defaultValue);
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse integer value.", key, value, e);
        }
    }

    /**
     * Gets values as a list of integers.
     * @param key property key
     * @return the values
     */
    public final List<Integer> getInts(String key) {
        List<String> values = getStrings(key);
        String errVal = null;
        try {
            List<Integer> ints = new ArrayList<Integer>(values.size());
            for (String value : values) {
                errVal = value;
                ints.add(Integer.parseInt(value));
            }
            return ints;
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse integer value.", key, errVal, e);
        }
    }

    /**
     * Sets one or multiple integer values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setInt(String key, int... values) {
        setString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    /**
     * Adds one or multiple integer values values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addInt(String key, int... values) {
        addString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    //--- Double ---------------------------------------------------------------
    /**
     * Gets value as a double.
     * @param key property key
     * @return the value
     */
    public final double getDouble(String key) {
        try {
            return Double.parseDouble(getString(key));
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse double value.", key, getString(key), e);
        }
    }

    /**
     * Gets value as a double.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final double getDouble(String key, double defaultValue) {
        String value = getString(key, "" + defaultValue);
        try {
            return Double.parseDouble(value);
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse double value.", key, value, e);
        }
    }

    /**
     * Gets values as a list of doubles.
     * @param key property key
     * @return the values
     */
    public final List<Double> getDoubles(String key) {
        List<String> values = getStrings(key);
        String errVal = null;
        try {
            List<Double> list = new ArrayList<Double>(values.size());
            for (String value : values) {
                errVal = value;
                list.add(Double.parseDouble(value));
            }
            return list;
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse double value.", key, errVal, e);
        }
    }

    /**
     * Sets one or multiple double values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setDouble(String key, double... values) {
        setString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    /**
     * Adds one or multiple double values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addDouble(String key, double... values) {
        addString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    //--- Long -----------------------------------------------------------------
    /**
     * Gets value as a long.
     * @param key property key
     * @return the value
     */
    public final long getLong(String key) {
        try {
            return Long.parseLong(getString(key));
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse long value.", key, getString(key), e);
        }
    }

    /**
     * Gets value as a long.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final long getLong(String key, long defaultValue) {
        String value = getString(key, "" + defaultValue);
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse long value.", key, value, e);
        }
    }

    /**
     * Gets values as a list of longs.
     * @param key property key
     * @return the values
     */
    public final List<Long> getLongs(String key) {
        List<String> values = getStrings(key);
        String errVal = null;
        try {
            List<Long> list = new ArrayList<Long>(values.size());
            for (String value : values) {
                errVal = value;
                list.add(Long.parseLong(value));
            }
            return list;
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse long value.", key, errVal, e);
        }
    }

    /**
     * Sets one or multiple long values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setLong(String key, long... values) {
        setString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    /**
     * Add one or multiple long values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addLong(String key, long... values) {
        addString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    //--- Float ----------------------------------------------------------------
    /**
     * Gets value as a float.
     * @param key property key
     * @return the value
     */
    public final float getFloat(String key) {
        try {
            return Float.parseFloat(getString(key));
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse float value.", key, getString(key), e);
        }
    }

    /**
     * Gets value as a float.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final float getFloat(String key, float defaultValue) {
        String value = getString(key, "" + defaultValue);
        try {
            return Float.parseFloat(value);
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse float value.", key, value, e);
        }
    }

    /**
     * Gets values as a list of floats.
     * @param key property key
     * @return the values
     */
    public final List<Float> getFloats(String key) {
        List<String> values = getStrings(key);
        String errVal = null;
        try {
            List<Float> list = new ArrayList<Float>(values.size());
            for (String value : values) {
                errVal = value;
                list.add(Float.parseFloat(value));
            }
            return list;
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse float value.", key, errVal, e);
        }
    }

    /**
     * Sets one or multiple float values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setFloat(String key, float... values) {
        setString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    /**
     * Adds one or multiple long values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addFloat(String key, float... values) {
        addString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    //--- BigDecimal -----------------------------------------------------------
    /**
     * Gets value as a BigDecimal.
     * @param key property key
     * @return the value
     */
    public final BigDecimal getBigDecimal(String key) {
        String value = getString(key);
        if (value == null || value.trim().length() == 0) {
            return null;
        }
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse BigDecimal value.", key, value, e);
        }
    }

    /**
     * Gets value as a BigDecimal.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final BigDecimal getBigDecimal(String key, BigDecimal defaultValue) {
        BigDecimal value = getBigDecimal(key);
        if (value == null) {
            return defaultValue;
        }
        return value;
    }

    /**
     * Gets values as a list of BigDecimals.
     * @param key property key
     * @return the values
     */
    public final List<BigDecimal> getBigDecimals(String key) {
        List<String> values = getStrings(key);
        String errVal = null;
        try {
            List<BigDecimal> list = new ArrayList<BigDecimal>(values.size());
            for (String value : values) {
                errVal = value;
                list.add(new BigDecimal(value));
            }
            return list;
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse BigDecimal value.", key, errVal, e);
        }
    }

    /**
     * Sets one or multiple BigDecimal values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setBigDecimal(String key, BigDecimal... values) {
        setString(key, toStringArray(values));
    }

    /**
     * Add one or multiple BigDecimal values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addBigDecimal(String key, BigDecimal... values) {
        addString(key, toStringArray(values));
    }

    //--- Date -----------------------------------------------------------------
    /**
     * Gets value as a date.
     * @param key property key
     * @return the value
     */
    public final Date getDate(String key) {
        String value = getString(key);
        if (StringUtils.isBlank(value)) {
            return null;
        }
        try {
            return new Date(Long.parseLong(value));
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse Date value.", key, value, e);
        }
    }

    /**
     * Gets value as a date.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final Date getDate(String key, Date defaultValue) {
        Date value = getDate(key);
        if (value == null) {
            return defaultValue;
        }
        return value;
    }

    /**
     * Gets values as a list of dates.
     * @param key property key
     * @return the values
     */
    public final List<Date> getDates(String key) {
        List<String> values = getStrings(key);
        String errVal = null;
        try {
            List<Date> list = new ArrayList<Date>(values.size());
            for (String value : values) {
                errVal = value;
                list.add(new Date(Long.parseLong(value)));
            }
            return list;
        } catch (NumberFormatException e) {
            throw createTypedException("Could not parse Date value.", key, errVal, e);
        }
    }

    /**
     * Sets one or multiple date values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setDate(String key, Date... values) {
        setString(key, datesToStringArray(values));
    }

    /**
     * Add one or multiple date values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addDate(String key, Date... values) {
        addString(key, datesToStringArray(values));
    }

    private String[] datesToStringArray(Date... values) {
        if (values == null) {
            return null;
        }
        String[] array = new String[values.length];
        for (int i = 0; i < array.length; i++) {
            array[i] = Long.toString(values[i].getTime());
        }
        return array;
    }

    //--- Boolean --------------------------------------------------------------
    /**
     * Gets value as a boolean.
     * @param key property key
     * @return the value
     */
    public final boolean getBoolean(String key) {
        return Boolean.valueOf(getString(key)).booleanValue();
    }

    /**
     * Gets value as a boolean.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final boolean getBoolean(String key, boolean defaultValue) {
        return Boolean.valueOf(getString(key, "" + defaultValue)).booleanValue();
    }

    /**
     * Gets values as a list of booleans.
     * @param key property key
     * @return the values
     */
    public final List<Boolean> getBooleans(String key) {
        List<String> values = getStrings(key);
        List<Boolean> list = new ArrayList<Boolean>(values.size());
        for (String value : values) {
            list.add(Boolean.parseBoolean(value));
        }
        return list;
    }

    /**
     * Sets one or multiple boolean values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setBoolean(String key, boolean... values) {
        setString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    /**
     * Adds one or multiple boolean values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addBoolean(String key, boolean... values) {
        addString(key, toStringArray(ArrayUtils.toObject(values)));
    }

    //--- Locale ---------------------------------------------------------------
    /**
     * Gets value as a locale.
     * @param key property key
     * @return the value
     */
    public final Locale getLocale(String key) {
        try {
            return LocaleUtils.toLocale(getString(key));
        } catch (IllegalArgumentException e) {
            throw createTypedException("Could not parse Locale value.", key, getString(key), e);
        }
    }

    /**
     * Gets value as a locale.
     * @param key property key
     * @param defaultValue default value to return when original value is null.
     * @return the value
     */
    public final Locale getLocale(String key, Locale defaultValue) {
        try {
            return LocaleUtils.toLocale(getString(key));
        } catch (IllegalArgumentException e) {
            return defaultValue;
        }
    }

    /**
     * Gets values as a list of locales.
     * @param key property key
     * @return the values
     */
    public final List<Locale> getLocales(String key) {
        List<String> values = getStrings(key);
        String errVal = null;
        try {
            List<Locale> list = new ArrayList<Locale>(values.size());
            for (String value : values) {
                errVal = value;
                list.add(LocaleUtils.toLocale(value));
            }
            return list;
        } catch (IllegalArgumentException e) {
            throw createTypedException("Could not parse locale value.", key, errVal, e);
        }
    }

    /**
     * Sets one or multiple locale values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setLocale(String key, Locale... values) {
        setString(key, toStringArray(values));
    }

    /**
     * Adds one or multiple locale values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addLocale(String key, Locale... values) {
        addString(key, toStringArray(values));
    }

    //--- File -----------------------------------------------------------------
    /**
     * Gets a file, assuming key value is a file system path. 
     * @param key properties key
     * @return a File
     */
    public final File getFile(String key) {
        String filePath = getString(key);
        if (filePath == null) {
            return null;
        }
        return new File(filePath);
    }

    /**
     * Gets a file, assuming key value is a file system path. 
     * @param key properties key
     * @param defaultValue default file being returned if no file has been
     *        defined for the given key in the properties.
     * @return a File
     */
    public final File getFile(String key, File defaultValue) {
        File value = getFile(key);
        if (value == null) {
            return defaultValue;
        }
        return value;
    }

    /**
     * Gets values as a list of files.
     * @param key property key
     * @return the values
     */
    public final List<File> getFiles(String key) {
        List<String> values = getStrings(key);
        List<File> list = new ArrayList<File>(values.size());
        for (String value : values) {
            list.add(new File(value));
        }
        return list;
    }

    /**
     * Sets one or multiple file values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setFile(String key, File... values) {
        setString(key, filesToStringArray(values));
    }

    /**
     * Adds one or multiple file values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addFile(String key, File... values) {
        addString(key, filesToStringArray(values));
    }

    private String[] filesToStringArray(File... values) {
        if (values == null) {
            return null;
        }
        String[] array = new String[values.length];
        for (int i = 0; i < array.length; i++) {
            array[i] = values[i].getPath();
        }
        return array;
    }

    //--- Class ----------------------------------------------------------------
    /**
     * Gets a class, assuming key value is a fully qualified class name
     * available in the classloader. 
     * @param key properties key
     * @return initialized class
     */
    public final Class<?> getClass(String key) {
        String value = getString(key);
        try {
            return Class.forName(value);
        } catch (ClassNotFoundException e) {
            throw createTypedException("Could not parse class value.", key, value, e);
        }
    }

    /**
     * Gets a class, assuming key value is a fully qualified class name
     * available in the classloader. 
     * @param key properties key
     * @param defaultValue default file being returned if no class has been
     *        defined for the given key in the properties.
     * @return initialized class
     */
    public final Class<?> getClass(String key, Class<?> defaultValue) {
        Class<?> value = getClass(key);
        if (value == null) {
            return defaultValue;
        }
        return value;
    }

    /**
     * Gets values as a list of initialized classes.
     * @param key property key
     * @return the values
     */
    public final List<Class<?>> getClasses(String key) {
        List<String> values = getStrings(key);
        List<Class<?>> list = new ArrayList<Class<?>>(values.size());
        for (String value : values) {
            list.add(getClass(value));
        }
        return list;
    }

    /**
     * Sets one or multiple class values, replacing existing ones.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void setClass(String key, Class<?>... values) {
        setString(key, classesToStringArray(values));
    }

    /**
     * Adds one or multiple class values.  
     * @param key the key of the values to set
     * @param values the values to set
     */
    public final void addClass(String key, Class<?>... values) {
        addString(key, classesToStringArray(values));
    }

    //--- Other ----------------------------------------------------------------
    @Override
    public final List<String> get(Object key) {
        if (!caseSensitiveKeys) {
            return super.get(key);
        }
        List<String> values = new ArrayList<String>();
        for (String k : keySet()) {
            if (StringUtils.equalsIgnoreCase(k, Objects.toString(key, null))) {
                values.addAll(super.get(k));
            }
        }
        return values;
    }

    @Override
    public final List<String> remove(Object key) {

        if (!caseSensitiveKeys) {
            return super.remove(key);
        }

        List<String> oldValues = null;
        List<String> keysToRemove = new ArrayList<String>();
        for (Iterator<String> it = keySet().iterator(); it.hasNext();) {
            String k = it.next();
            if (StringUtils.equalsIgnoreCase(k, Objects.toString(key, null))) {
                keysToRemove.add(k);
            }
        }
        for (String k : keysToRemove) {
            List<String> previous = super.remove(k);
            if (previous != null) {
                if (oldValues == null) {
                    oldValues = new ArrayList<>();
                }
                oldValues.addAll(previous);
            }
        }
        return oldValues;
    }

    //--- Privates -------------------------------------------------------------
    private String[] classesToStringArray(Class<?>... values) {
        if (values == null) {
            return null;
        }
        String[] array = new String[values.length];
        for (int i = 0; i < array.length; i++) {
            array[i] = values[i].getName();
        }
        return array;
    }

    private PropertiesException createTypedException(String msg, String key, String value, Exception cause) {
        String message = msg + " [key=" + key + "; value=" + value + "].";
        LOG.error(message, cause);
        return new PropertiesException(message, cause);
    }

    private String[] toStringArray(Object[] array) {
        if (array == null) {
            return null;
        }
        String[] strArray = new String[array.length];
        for (int i = 0; i < array.length; i++) {
            strArray[i] = Objects.toString(array[i], StringUtils.EMPTY);

        }
        return strArray;
    }
}