com.mgmtp.jfunk.common.util.ExtendedProperties.java Source code

Java tutorial

Introduction

Here is the source code for com.mgmtp.jfunk.common.util.ExtendedProperties.java

Source

/*
 * Copyright (c) 2015 mgm technology partners GmbH
 *
 * 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.mgmtp.jfunk.common.util;

import java.io.BufferedWriter;
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.Writer;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.concurrent.NotThreadSafe;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * <p>
 * Properties implementation that does not have the encoding restrictions of
 * {@link java.util.Properties}.
 * </p>
 * Note that all operations consider potential defaults, e. g. the results returned by methods such
 * as {@link #size()}, {@link #keySet}, or {@link #entrySet} always consider potential defaults.
 * This in turn, however, means that removal operations also affect defaults (e. g. {@link #clear()}
 * ).
 * 
 */
@NotThreadSafe
public class ExtendedProperties implements Map<String, String>, Serializable, Cloneable {

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private static final long serialVersionUID = 1L;

    private static final ThreadLocal<List<String>> TOKEN = new ThreadLocal<List<String>>() {
        @Override
        protected List<String> initialValue() {
            return Lists.newArrayListWithCapacity(256);
        }
    };

    private Map<String, String> propsMap;
    private Map<String, String> defaults;

    private transient Set<String> keySet;
    private transient Set<Entry<String, String>> entrySet;
    private transient Collection<String> values;

    /**
     * Creates an empty instance.
     */
    public ExtendedProperties() {
        propsMap = new MapMaker().makeMap();
    }

    /**
     * Creates an empty instance with the specified defaults.
     * 
     * @param defaults
     *            The defaults
     */
    public ExtendedProperties(final Map<String, String> defaults) {
        this();
        this.defaults = defaults;
    }

    /**
     * Created an instance from the specified {@link java.util.Properties} instance.
     * 
     * @param props
     *            The properties instance
     * @return The {@link ExtendedProperties} instance
     */
    public static ExtendedProperties fromProperties(final Properties props) {
        ExtendedProperties result = new ExtendedProperties();
        for (Enumeration<?> en = props.propertyNames(); en.hasMoreElements();) {
            String key = (String) en.nextElement();
            String value = props.getProperty(key);
            result.put(key, value);
        }
        return result;
    }

    /**
     * Removes all properties and potential defaults.
     */
    @Override
    public void clear() {
        propsMap.clear();
        defaults = null;
    }

    /**
     * Looks up a property. Recursively checks the defaults if necessary. Internally,
     * {@code get(key, true)} is called to look up the value.
     * 
     * @param key
     *            The property key
     * @return The property value, or {@code null} if the property is not found
     */
    @Override
    public String get(final Object key) {
        return get(key, true);
    }

    /**
     * Looks up a property. Recursively checks the defaults if necessary. Internally,
     * {@code get(key, true)} is called to look up the value.
     * 
     * @param key
     *            The property key
     * @param defaultValue
     *            The value returned if the property is not found
     * @return The property value, or the specified default value if the property is not found
     */
    public String get(final Object key, final String defaultValue) {
        String val = get(key, true);
        return val == null ? defaultValue : val;
    }

    /**
     * Looks up a property. Recursively checks the defaults if necessary. If the property is found
     * as a system property, this value will be used.
     * 
     * @param key
     *            The property key
     * @param process
     *            If {@code true}, the looked-up value is passed to
     *            {@link #processPropertyValue(String)}, and the processed result is returned.
     * @return The property value, or {@code null} if the property is not found
     */
    public String get(final Object key, final boolean process) {
        String value = propsMap.get(key);
        if (process) {
            value = processPropertyValue(value);
        }
        return value == null && defaults != null ? defaults.get(key) : value;
    }

    public boolean getBoolean(final String key) {
        return Boolean.parseBoolean(get(key, ""));
    }

    public boolean getBoolean(final String key, final boolean defaultValue) {
        String value = get(key);
        return StringUtils.isNotEmpty(value) ? Boolean.parseBoolean(value) : defaultValue;
    }

    public long getLong(final String key) {
        String value = get(key);
        return Long.parseLong(value);
    }

    public long getLong(final String key, final long defaultValue) {
        String value = get(key);
        return StringUtils.isNotEmpty(value) ? Long.parseLong(value) : defaultValue;
    }

    public int getInteger(final String key) {
        String value = get(key);
        return Integer.parseInt(value);
    }

    public int getInteger(final String key, final int defaultValue) {
        String value = get(key);
        return StringUtils.isNotEmpty(value) ? Integer.parseInt(value) : defaultValue;
    }

    /**
     * Sets the property with the specified key.
     * 
     * @param key
     *            The key (may not be {@code null})
     * @param value
     *            The value (may not be {@code null})
     * @return The previous value of the property, or {@code null} if it did not have one
     */
    @Override
    public String put(final String key, final String value) {
        return propsMap.put(key, value);
    }

    /**
     * Return {@code true} if the property with the specified key exists. Defaults are considered in
     * the result.
     * 
     * @param key
     *            The key
     * @return {@code true} if the property with the specified key exists
     */
    @Override
    public boolean containsKey(final Object key) {
        return propsMap.containsKey(key) ? true : defaults != null && defaults.containsKey(key);
    }

    /**
     * Return {@code true} if a property with the specified value exists. Defaults are considered in
     * the result.
     * 
     * @param value
     *            The value
     * @return {@code true} if a property with the specified value exists
     */
    @Override
    public boolean containsValue(final Object value) {
        for (String key : keySet()) {
            if (value.equals(get(key))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns a set of Entries representing property key-value pairs. The returned set is a view to
     * the internal data structures and reflects changes to this instance. Potential defaults are
     * included in the returned set.
     * 
     * @return The entry set
     */
    @Override
    public Set<Entry<String, String>> entrySet() {
        if (entrySet == null) {
            entrySet = new EntrySet();
        }
        return entrySet;
    }

    /**
     * Returns {@code true} if no properties are available. Defaults are considered in the result.
     * 
     * @return {@code true} if no properties are available
     */
    @Override
    public boolean isEmpty() {
        return size() == 0;
    }

    /**
     * Adds all key-value pairs from the specified map.
     * 
     * @param map
     *            The map
     */
    @Override
    public void putAll(final Map<? extends String, ? extends String> map) {
        for (Entry<? extends String, ? extends String> entry : map.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Removes the property with the specified key. Defaults are considered.
     * 
     * @param key
     *            The key
     * @return The previous value associated with the property
     */
    @Override
    public String remove(final Object key) {
        String result = propsMap.remove(key);
        if (defaults != null) {
            String s = defaults.remove(key);
            if (result == null) {
                result = s;
            }
        }
        return result;
    }

    /**
     * Returns a collection of property values. The returned collection is a view to the internal
     * data structures and reflects changes to this instance. Potential defaults are included in the
     * returned collection.
     * 
     * @return The values collection
     */
    @Override
    public Collection<String> values() {
        if (values == null) {
            values = new Values();
        }
        return values;
    }

    /**
     * Returns a set of property keys. The returned set is a view to the internal data structures
     * and reflects changes to this instance. Potential defaults are included in the returned set.
     * 
     * @return The key set
     */
    @Override
    public Set<String> keySet() {
        if (keySet == null) {
            keySet = new KeySet();
        }
        return keySet;
    }

    /**
     * Calculates and returns the number of properties. Defaults are considered in the result.
     * 
     * @return The number of properties
     */
    @Override
    public int size() {
        return keySet().size();
    }

    /**
     * <p>
     * Replaces tokens like <code>${xxx}</code> contain in {@code input} with the value of the
     * property with the key {@code xxx}, or the system property with that key. In case of no match,
     * the tokens remain unchanged.
     * </p>
     * <p>
     * A default value may be specified separated by a comma from the property key:
     * <code>${xxx,yyy}</code>. The default value may be the empty string.
     * </p>
     * <p>
     * Example:<br />
     * If neither a property nor a system property with the key {@code archive.dir} is found,
     * <code>c:/temp/${archive.dir,dummy_test}/testruns</code>, the default value "dummy_test" is
     * resolved for <code>${archive.dir,dummy_test}<code> and the result is {@code
     * c:/temp/dummy_test/testruns}.
     * </p>
     * <p>
     * Tokens may be nested, e. g. <code>${key${user.dir,c:/${current.dir}/archive},default}}</code>
     * .
     * </p>
     * 
     * @param input
     *            The input string
     * @return The processed value
     */
    public String processPropertyValue(final String input) {
        if (input == null) {
            return null;
        }
        // Check if there is anything to do at all...
        int startIndex = input.indexOf("${");
        if (startIndex == -1) {
            return input;
        }
        if (input.indexOf('}', startIndex) == -1) {
            return input;
        }
        String prefix;
        String property;
        String postfix;
        // as the first tag has been searched the beginning is tagfree
        prefix = input.substring(0, startIndex);
        // Search for a matching brace for the Open tag
        int openTags = 1;
        int endIndex = startIndex;
        while (endIndex < input.length() - 1 && openTags > 0) {
            endIndex++;
            if (input.charAt(endIndex) == '}') {
                openTags--;
            } else if (input.charAt(endIndex) == '$' && input.charAt(endIndex + 1) == '{') {
                openTags++;
            }
        }
        // if the openTag is not matching a closing tag we will return it as is
        if (openTags > 0) {
            return prefix + "${" + processPropertyValue(input.substring(startIndex + 2));
        }
        // register token to catch infinite loops
        List<String> tokens = TOKEN.get();
        if (tokens.contains(input)) {
            StringBuilder buffer = new StringBuilder(256);
            for (String s : tokens) {
                buffer.append(s);
                buffer.append("-->");
            }
            throw new IllegalStateException("The string '" + input
                    + "' is already being processed; this results in a short circuit: " + buffer);
        }
        tokens.add(input);
        // now the startIndex is at the nun  ${ and endIndex at the associated }, so process the internal
        // first process that inside of the tags
        String inner = processPropertyValue(input.substring(startIndex + 2, endIndex));
        int index = inner.indexOf(',');
        String key;
        if (index == -1) {
            // kein default replacement
            key = inner;
        } else {
            // default replacement
            key = inner.substring(0, index);
        }
        property = null;
        if (key.length() == 0) {
            logger.warn("'" + input + "' contains an empty placeholder token ${...} which is not allowed");
        } else {
            property = get(key);
        }
        // no value found; default value
        if (property == null) {
            if (index == -1) {
                // kein default inner, so return everything
                property = "${" + key + "}";
            } else {
                property = inner.substring(index + 1);
            }
        }
        // the string part after the tag still has to be processed
        postfix = processPropertyValue(input.substring(endIndex + 1));
        // remove token as this here is done
        tokens.remove(input);
        // assemble and return value
        String result = prefix + property + postfix;
        logger.debug("Processing property {} to {}", input, result);
        return result;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        ExtendedProperties other = (ExtendedProperties) obj;
        return entrySet().equals(other.entrySet());
    }

    @Override
    public int hashCode() {
        int hashCode = 0;
        for (Entry<String, String> entry : entrySet()) {
            hashCode += entry.hashCode();
        }
        return hashCode;
    }

    /**
     * Returns a new {@link java.util.Properties} instance representing the properties.
     * 
     * @return The properties
     */
    public Properties toProperties() {
        Properties props = new Properties();
        // We need to iterate over the keys calling
        // getProperty in order to consider defaults.
        for (String key : keySet()) {
            props.put(key, get(key));
        }
        return props;
    }

    /**
     * Returns a string representation of this instance. Returns all properties recursively
     * including defaults.
     * 
     * @return The string representation of the instance
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(propsMap);
        if (defaults != null) {
            sb.append("\nDefaults: ");
            sb.append(defaults);
        }
        return sb.toString();
    }

    /**
     * Loads properties from the specified stream using the default encoding. The caller of this
     * method is responsible for closing the stream.
     * 
     * @param is
     *            The input stream
     */
    public void load(final InputStream is) throws IOException {
        load(new InputStreamReader(is));
    }

    /**
     * Loads properties from the specified stream. The caller of this method is responsible for
     * closing the stream.
     * 
     * @param is
     *            The input stream
     * @param encoding
     *            The encoding
     */
    public void load(final InputStream is, final String encoding) throws IOException {
        load(new InputStreamReader(is, encoding));
    }

    /**
     * Loads properties from the specified reader. The caller of this method is responsible for
     * closing the reader.
     * 
     * @param reader
     *            The reader
     */
    public void load(final Reader reader) throws IOException {
        doLoad(new LineReader(reader));
    }

    private void doLoad(final LineReader lr) throws IOException {
        char[] convtBuf = new char[1024];
        int limit;
        int keyLen;
        int valueStart;
        char c;
        boolean hasSep;
        boolean precedingBackslash;

        while ((limit = lr.readLine()) >= 0) {
            c = 0;
            keyLen = 0;
            valueStart = limit;
            hasSep = false;

            precedingBackslash = false;
            while (keyLen < limit) {
                c = lr.lineBuf[keyLen];
                //need check if escaped.
                if ((c == '=' || c == ':') && !precedingBackslash) {
                    valueStart = keyLen + 1;
                    hasSep = true;
                    break;
                } else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) {
                    valueStart = keyLen + 1;
                    break;
                }
                if (c == '\\') {
                    precedingBackslash = !precedingBackslash;
                } else {
                    precedingBackslash = false;
                }
                keyLen++;
            }
            while (valueStart < limit) {
                c = lr.lineBuf[valueStart];
                if (c != ' ' && c != '\t' && c != '\f') {
                    if (!hasSep && (c == '=' || c == ':')) {
                        hasSep = true;
                    } else {
                        break;
                    }
                }
                valueStart++;
            }
            String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf);
            String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf);
            put(key, value);
        }
    }

    /**
     * Writes the properties to the specified stream using the default encoding, including defaults.
     * 
     * @param os
     *            The output stream
     * @param comments
     *            Header comment that is written to the stream
     * @param sorted
     *            If {@code true}, the properties are written sorted by key
     * @param process
     *            If {@code true}, place holders are resolved
     */
    public void store(final OutputStream os, final String comments, final boolean sorted, final boolean process)
            throws IOException {
        store(new OutputStreamWriter(os), comments, sorted, process);
    }

    /**
     * Writes the properties to the specified stream, including defaults.
     * 
     * @param os
     *            The output stream
     * @param encoding
     *            The encoding
     * @param comments
     *            The header comment written to the stream
     * @param sorted
     *            If {@code true}, the properties are written sorted by key
     * @param process
     *            If {@code true}, place holders are resolved
     */
    public void store(final OutputStream os, final String encoding, final String comments, final boolean sorted,
            final boolean process) throws IOException {
        store(new OutputStreamWriter(os, encoding), comments, sorted, process);
    }

    /**
     * Writes the properties to the specified writer, including defaults.
     * 
     * @param writer
     *            The writer
     * @param comments
     *            The header comment written to the writer
     * @param sorted
     *            If {@code true}, the properties are written sorted by key
     * @param process
     *            If {@code true}, place holders are resolved
     */
    public void store(final Writer writer, final String comments, final boolean sorted, final boolean process)
            throws IOException {
        BufferedWriter bw = writer instanceof BufferedWriter ? (BufferedWriter) writer : new BufferedWriter(writer);

        if (comments != null) {
            for (Scanner scanner = new Scanner(comments); scanner.hasNextLine();) {
                bw.write("#");
                bw.write(scanner.nextLine());
                bw.newLine();
            }
        }

        bw.write("#" + new Date());
        bw.newLine();
        Set<String> keys = keySet();
        if (sorted) {
            keys = Sets.newTreeSet(keys);
        }

        for (String key : keys) {
            /*
             * No need to escape embedded and trailing spaces for value, hence pass false to flag.
             */
            bw.write(saveConvert(key, true) + "=" + saveConvert(get(key, process), false));
            bw.newLine();
        }
        bw.flush();
    }

    @Override
    public ExtendedProperties clone() {
        try {
            ExtendedProperties clone = (ExtendedProperties) super.clone();
            clone.keySet = null;
            clone.entrySet = null;
            clone.values = null;
            clone.propsMap = new ConcurrentHashMap<String, String>(propsMap);
            if (defaults != null) {
                // This recursively clones defaults.
                if (defaults instanceof ExtendedProperties) {
                    clone.defaults = ((ExtendedProperties) defaults).clone();
                } else {
                    clone.defaults = Maps.newHashMap(defaults);
                }
            }
            return clone;
        } catch (CloneNotSupportedException ex) {
            // can't happen
            throw new IllegalStateException(ex);
        }
    }

    /*
     * Read in a "logical line" from an InputStream/Reader, skip all comment and blank lines and
     * filter out those leading whitespace characters ( , and ) from the beginning of a
     * "natural line". Method returns the char length of the "logical line" and stores the line in
     * "lineBuf".
     */
    // --> Copied and adapted from the JDK
    static class LineReader {
        public LineReader(final Reader reader) {
            this.reader = reader;
            inCharBuf = new char[8192];
        }

        char[] inCharBuf;
        char[] lineBuf = new char[1024];
        int inLimit = 0;
        int inOff = 0;
        Reader reader;

        int readLine() throws IOException {
            int len = 0;
            char c = 0;

            boolean skipWhiteSpace = true;
            boolean isCommentLine = false;
            boolean isNewLine = true;
            boolean appendedLineBegin = false;
            boolean precedingBackslash = false;
            boolean skipLF = false;

            while (true) {
                if (inOff >= inLimit) {
                    inLimit = reader.read(inCharBuf);
                    inOff = 0;
                    if (inLimit <= 0) {
                        if (len == 0 || isCommentLine) {
                            return -1;
                        }
                        return len;
                    }
                }

                c = inCharBuf[inOff++];
                if (skipLF) {
                    skipLF = false;
                    if (c == '\n') {
                        continue;
                    }
                }
                if (skipWhiteSpace) {
                    if (c == ' ' || c == '\t' || c == '\f') {
                        continue;
                    }
                    if (!appendedLineBegin && (c == '\r' || c == '\n')) {
                        continue;
                    }
                    skipWhiteSpace = false;
                    appendedLineBegin = false;
                }
                if (isNewLine) {
                    isNewLine = false;
                    if (c == '#' || c == '!') {
                        isCommentLine = true;
                        continue;
                    }
                }

                if (c != '\n' && c != '\r') {
                    lineBuf[len++] = c;
                    if (len == lineBuf.length) {
                        int newLength = lineBuf.length * 2;
                        if (newLength < 0) {
                            newLength = Integer.MAX_VALUE;
                        }
                        char[] buf = new char[newLength];
                        System.arraycopy(lineBuf, 0, buf, 0, lineBuf.length);
                        lineBuf = buf;
                    }
                    //flip the preceding backslash flag
                    if (c == '\\') {
                        precedingBackslash = !precedingBackslash;
                    } else {
                        precedingBackslash = false;
                    }
                } else {
                    // reached EOL
                    if (isCommentLine || len == 0) {
                        isCommentLine = false;
                        isNewLine = true;
                        skipWhiteSpace = true;
                        len = 0;
                        continue;
                    }
                    if (inOff >= inLimit) {
                        inLimit = reader.read(inCharBuf);
                        inOff = 0;
                        if (inLimit <= 0) {
                            return len;
                        }
                    }
                    if (precedingBackslash) {
                        len -= 1;
                        //skip the leading whitespace characters in following line
                        skipWhiteSpace = true;
                        appendedLineBegin = true;
                        precedingBackslash = false;
                        if (c == '\r') {
                            skipLF = true;
                        }
                    } else {
                        return len;
                    }
                }
            }
        }
    }

    /*
     * Converts encoded &#92;uxxxx to unicode chars and changes special saved chars to their
     * original forms
     */
    //--> Copied and adapted from the JDK
    private String loadConvert(final char[] in, int off, final int len, char[] convtBuf) {
        if (convtBuf.length < len) {
            int newLen = len * 2;
            if (newLen < 0) {
                newLen = Integer.MAX_VALUE;
            }
            convtBuf = new char[newLen];
        }
        char aChar;
        char[] out = convtBuf;
        int outLen = 0;
        int end = off + len;

        while (off < end) {
            aChar = in[off++];
            if (aChar == '\\') {
                aChar = in[off++];
                switch (aChar) {
                case 't':
                    aChar = '\t';
                    break;
                case 'r':
                    aChar = '\r';
                    break;
                case 'n':
                    aChar = '\n';
                    break;
                case 'f':
                    aChar = '\f';
                    break;
                }
                out[outLen++] = aChar;
            } else {
                out[outLen++] = aChar;
            }
        }
        return new String(out, 0, outLen);
    }

    /*
     * Escapes special characters with a preceding slash
     */
    //--> Copied and adapted from the JDK
    private String saveConvert(final String theString, final boolean escapeSpace) {
        int len = theString.length();
        int bufLen = len * 2;
        if (bufLen < 0) {
            bufLen = Integer.MAX_VALUE;
        }
        StringBuilder outBuffer = new StringBuilder(bufLen);

        for (int x = 0; x < len; x++) {
            char aChar = theString.charAt(x);
            // Handle common case first, selecting largest block that
            // avoids the specials below
            if (aChar > 61 && aChar < 127) {
                if (aChar == '\\') {
                    outBuffer.append('\\');
                    outBuffer.append('\\');
                    continue;
                }
                outBuffer.append(aChar);
                continue;
            }
            switch (aChar) {
            case ' ':
                if (x == 0 || escapeSpace) {
                    outBuffer.append('\\');
                }
                outBuffer.append(' ');
                break;
            case '\t':
                outBuffer.append('\\');
                outBuffer.append('t');
                break;
            case '\n':
                outBuffer.append('\\');
                outBuffer.append('n');
                break;
            case '\r':
                outBuffer.append('\\');
                outBuffer.append('r');
                break;
            case '\f':
                outBuffer.append('\\');
                outBuffer.append('f');
                break;
            case '=': // Fall through
            case ':': // Fall through
            case '#': // Fall through
            case '!':
                outBuffer.append('\\');
                outBuffer.append(aChar);
                break;
            default:
                outBuffer.append(aChar);
            }
        }
        return outBuffer.toString();
    }

    final class KeySet extends AbstractSet<String> {

        @Override
        public Iterator<String> iterator() {
            return new PropertiesIterator<String>(propsMap.keySet(), defaults != null ? defaults.keySet() : null,
                    false);
        }

        @Override
        public int size() {
            // We cannot used EnhancedProperties.this.size() because it relies on this method.
            Set<String> result = Sets.newHashSet(propsMap.keySet());
            if (defaults != null) {
                // Recursion using keySet()!
                result.addAll(defaults.keySet());
            }
            return result.size();
        }

        @Override
        public boolean contains(final Object o) {
            return containsKey(o);
        }

        @Override
        public boolean remove(final Object o) {
            return ExtendedProperties.this.remove(o) != null;
        }

        @Override
        public void clear() {
            ExtendedProperties.this.clear();
        }

        @Override
        public Object[] toArray() {
            Collection<String> c = toCollection();
            return c.toArray();
        }

        @Override
        public <T> T[] toArray(final T[] a) {
            Collection<String> c = toCollection();
            return c.toArray(a);
        }

        private Collection<String> toCollection() {
            Collection<String> c = Lists.newArrayList();
            for (String string : this) {
                c.add(string);
            }
            return c;
        }
    }

    final class Values extends AbstractCollection<String> {
        @Override
        public Iterator<String> iterator() {
            return new PropertiesIterator<String>(propsMap.values(), defaults != null ? defaults.values() : null,
                    true);
        }

        @Override
        public int size() {
            return ExtendedProperties.this.size();
        }

        @Override
        public boolean contains(final Object o) {
            return containsValue(o);
        }

        @Override
        public void clear() {
            ExtendedProperties.this.clear();
        }

        @Override
        public Object[] toArray() {
            Collection<String> c = toCollection();
            return c.toArray();
        }

        @Override
        public <T> T[] toArray(final T[] a) {
            Collection<String> c = toCollection();
            return c.toArray(a);
        }

        private Collection<String> toCollection() {
            Collection<String> c = Lists.newArrayList();
            for (String string : this) {
                c.add(string);
            }
            return c;
        }
    }

    final class EntrySet extends AbstractSet<Map.Entry<String, String>> {
        @Override
        public Iterator<Map.Entry<String, String>> iterator() {
            Set<Map.Entry<String, String>> defaultsEntrySet = defaults != null ? defaults.entrySet() : null;
            return new PropertiesIterator<Map.Entry<String, String>>(propsMap.entrySet(), defaultsEntrySet, false);
        }

        @Override
        public boolean contains(final Object o) {
            if (!(o instanceof Map.Entry<?, ?>)) {
                return false;
            }
            Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
            String value = ExtendedProperties.this.get(e.getKey());
            return value != null && value.equals(e.getValue());
        }

        @Override
        public boolean remove(final Object o) {
            if (!(o instanceof Map.Entry<?, ?>)) {
                return false;
            }
            Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
            return ExtendedProperties.this.remove(e.getKey()) != null;
        }

        @Override
        public int size() {
            return ExtendedProperties.this.size();
        }

        @Override
        public void clear() {
            ExtendedProperties.this.clear();
        }

        @Override
        public Object[] toArray() {
            Collection<Map.Entry<String, String>> c = toCollection();
            return c.toArray();
        }

        @Override
        public <T> T[] toArray(final T[] a) {
            Collection<Map.Entry<String, String>> c = toCollection();
            return c.toArray(a);
        }

        private Collection<Map.Entry<String, String>> toCollection() {
            Collection<Map.Entry<String, String>> c = Lists.newArrayListWithExpectedSize(size());
            for (Map.Entry<String, String> entry : this) {
                c.add(new SimpleEntry(entry));
            }
            return c;
        }
    }

    /**
     * Simple implementation, copied and slightly adapted from java.util.AbstractMap.SimpleEntry.
     */
    static final class SimpleEntry implements Entry<String, String> {
        String key;
        String value;

        public SimpleEntry(final String key, final String value) {
            this.key = key;
            this.value = value;
        }

        public SimpleEntry(final Entry<String, String> e) {
            key = e.getKey();
            value = e.getValue();
        }

        @Override
        public String getKey() {
            return key;
        }

        @Override
        public String getValue() {
            return value;
        }

        @Override
        public String setValue(final String value) {
            String oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        @Override
        public boolean equals(final Object o) {
            if (!(o instanceof Map.Entry<?, ?>)) {
                return false;
            }
            Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
            return eq(key, e.getKey()) && eq(value, e.getValue());
        }

        @Override
        public int hashCode() {
            return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
        }

        @Override
        public String toString() {
            return key + "=" + value;
        }

        boolean eq(final Object o1, final Object o2) {
            return o1 == null ? o2 == null : o1.equals(o2);
        }
    }

    /**
     * {@link Iterator} implementation considering a default collection.
     */
    static final class PropertiesIterator<T> implements Iterator<T> {

        private final Iterator<T> iter;
        private final Iterator<T> defaultsIter;
        private boolean defaultsMode;
        private final boolean readonlyIterator;

        PropertiesIterator(final Collection<T> coll, final Collection<T> defaultsColl,
                final boolean readonlyIterator) {
            iter = coll.iterator();
            if (defaultsColl != null) {
                defaultsIter = defaultsColl.iterator();
            } else {
                defaultsIter = null;
            }
            this.readonlyIterator = readonlyIterator;
        }

        @Override
        public boolean hasNext() {
            if (iter.hasNext()) {
                return true;
            }
            if (defaultsIter != null) {
                defaultsMode = true;
                return defaultsIter.hasNext();
            }
            return false;
        }

        @Override
        public T next() {
            if (iter.hasNext()) {
                return iter.next();
            }
            if (defaultsIter != null) {
                defaultsMode = true;
                return defaultsIter.next();
            }
            throw new NoSuchElementException();
        }

        @Override
        public void remove() {
            if (readonlyIterator) {
                throw new UnsupportedOperationException("Iterator is read-only.");
            }
            if (defaultsMode) {
                defaultsIter.remove();
            } else {
                iter.remove();
            }
        }
    }
}