You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by oh...@apache.org on 2009/10/31 17:11:38 UTC

svn commit: r831560 - in /commons/proper/configuration/branches/configuration2_experimental/src: main/java/org/apache/commons/configuration2/base/impl/ test/java/org/apache/commons/configuration2/base/impl/

Author: oheger
Date: Sat Oct 31 16:11:38 2009
New Revision: 831560

URL: http://svn.apache.org/viewvc?rev=831560&view=rev
Log:
Added INIConfigurationSource as a proof of concept for a file-based configuration source.

Added:
    commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/
    commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java   (with props)
    commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/
    commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java   (with props)

Added: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java?rev=831560&view=auto
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java (added)
+++ commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java Sat Oct 31 16:11:38 2009
@@ -0,0 +1,643 @@
+/*
+ * 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.commons.configuration2.base.impl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.configuration2.ConfigurationException;
+import org.apache.commons.configuration2.base.Capability;
+import org.apache.commons.configuration2.base.DefaultLocatorSupport;
+import org.apache.commons.configuration2.base.InMemoryConfigurationSource;
+import org.apache.commons.configuration2.base.LocatorSupport;
+import org.apache.commons.configuration2.base.StreamBasedSource;
+import org.apache.commons.configuration2.expr.ExpressionEngine;
+import org.apache.commons.configuration2.tree.ConfigurationNode;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * <p>
+ * A specialized hierarchical configuration source implementation for parsing
+ * ini files.
+ * </p>
+ * <p>
+ * An initialization or ini file is a configuration file typically found on
+ * Microsoft's Windows operating system and contains data for Windows based
+ * applications.
+ * </p>
+ * <p>
+ * Although popularized by Windows, ini files can be used on any system or
+ * platform due to the fact that they are merely text files that can easily be
+ * parsed and modified by both humans and computers.
+ * </p>
+ * <p>
+ * A typcial ini file could look something like:
+ * </p>
+ * <code>
+ * [section1]<br>
+ * ; this is a comment!<br>
+ * var1 = foo<br>
+ * var2 = bar<br>
+ * <br>
+ * [section2]<br>
+ * var1 = doo<br>
+ * </code>
+ * <p>
+ * The format of ini files is fairly straight forward and is composed of three
+ * components:<br>
+ * <ul>
+ * <li><b>Sections:</b> Ini files are split into sections, each section starting
+ * with a section declaration. A section declaration starts with a '[' and ends
+ * with a ']'. Sections occur on one line only.</li>
+ * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters
+ * have a typical <code>key = value</code> format.</li>
+ * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * There are various implementations of the ini file format by various vendors
+ * which has caused a number of differences to appear. As far as possible this
+ * configuration source tries to be lenient and support most of the differences.
+ * </p>
+ * <p>
+ * Some of the differences supported are as follows:
+ * <ul>
+ * <li><b>Comments:</b> The '#' character is also accepted as a comment
+ * signifier.</li>
+ * <li><b>Key value separtor:</b> The ':' character is also accepted in place of
+ * '=' to separate keys and values in parameters, for example
+ * <code>var1 : foo</code>.</li>
+ * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed,
+ * this configuration source does however support it. In the event of a
+ * duplicate section, the two section's values are merged.</li>
+ * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
+ * allowed if they are in two different sections, thus they are local to
+ * sections; this configuration source simply merges duplicates; if a section
+ * has a duplicate parameter the values are then added to the key as a list.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Global parameters are also allowed; any parameters declared before a section
+ * is declared are added to a global section. It is important to note that this
+ * global section does not have a name.
+ * </p>
+ * <p>
+ * In all instances, a parameter's key is prepended with its section name and a
+ * '.' (period). Thus a parameter named "var1" in "section1" will have the key
+ * <code>section1.var1</code> in this configuration source. (This is the default
+ * behavior. Because this is a hierarchical configuration source you can change
+ * this by setting a different {@link ExpressionEngine}.)
+ * </p>
+ * <p>
+ * <h3>Implementation Details:</h3> Consider the following ini file:<br>
+ * <code>
+ *  default = ok<br>
+ *  <br>
+ *  [section1]<br>
+ *  var1 = foo<br>
+ *  var2 = doodle<br>
+ *   <br>
+ *  [section2]<br>
+ *  ; a comment<br>
+ *  var1 = baz<br>
+ *  var2 = shoodle<br>
+ *  bad =<br>
+ *  = worse<br>
+ *  <br>
+ *  [section3]<br>
+ *  # another comment<br>
+ *  var1 : foo<br>
+ *  var2 : bar<br>
+ *  var5 : test1<br>
+ *  <br>
+ *  [section3]<br>
+ *  var3 = foo<br>
+ *  var4 = bar<br>
+ *  var5 = test2<br>
+ *  </code>
+ * </p>
+ * <p>
+ * This ini file will be parsed without error. Note:
+ * <ul>
+ * <li>The parameter named "default" is added to the global section, it's value
+ * is accessed simply using <code>getProperty("default")</code>.</li>
+ * <li>Section 1's parameters can be accessed using
+ * <code>getProperty("section1.var1")</code>.</li>
+ * <li>The parameter named "bad" simply adds the parameter with an empty value.</li>
+ * <li>The empty key with value "= worse" is added using a key consisting of a
+ * single space character. This key is still added to section 2 and the value
+ * can be accessed using <code>getProperty("section2. ")</code>, notice the
+ * period '.' and the space following the section name.</li>
+ * <li>Section three uses both '=' and ':' to separate keys and values.</li>
+ * <li>Section 3 has a duplicate key named "var5". The value for this key is
+ * [test1, test2], and is represented as a List.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Internally, this configuration source maps the content of the represented ini
+ * file to its node structure in the following way:
+ * <ul>
+ * <li>Sections are represented by direct child nodes of the root node.</li>
+ * <li>For the content of a section, corresponding nodes are created as children
+ * of the section node.</li>
+ * </ul>
+ * This explains how the keys for the properties can be constructed. You can
+ * also use other methods of {@link InMemoryConfigurationSource} for querying or
+ * manipulating the hierarchy of configuration nodes or use the extended {@code
+ * Configuration} interface. For instance, the {@code configurationAt()} method
+ * is useful for obtaining the data of a specific section.
+ * </p>
+ * <p>
+ * The set of sections in this configuration source can be retrieved using the
+ * {@code getSections()} method. For obtaining a {@link SubConfiguration} with
+ * the content of a specific section the {@code getSection()} method can be
+ * used.
+ * </p>
+ * <p>
+ * <em>Note:</em> Configuration source objects of this type can be read
+ * concurrently by multiple threads. However if one of these threads modifies
+ * the object, synchronization has to be performed manually.
+ * </p>
+ *
+ * @author <a
+ *         href="http://commons.apache.org/configuration/team-list.html">Commons
+ *         Configuration team</a>
+ * @version $Id$
+ */
+public class INIConfigurationSource extends InMemoryConfigurationSource
+        implements StreamBasedSource
+{
+    /**
+     * The characters that signal the start of a comment line.
+     */
+    private static final String COMMENT_CHARS = "#;";
+
+    /**
+     * Constant for the line separator.
+     */
+    private static final String LINE_SEPARATOR = System
+            .getProperty("line.separator");
+
+    /**
+     * The line continuation character.
+     */
+    private static final String LINE_CONT = "\\";
+
+    /**
+     * Return a set containing the sections in this ini configuration. Note that
+     * changes to this set do not affect the configuration.
+     *
+     * @return a set containing the sections.
+     */
+    public Set<String> getSections()
+    {
+        Set<String> sections = new LinkedHashSet<String>();
+        boolean globalSection = false;
+
+        for (ConfigurationNode node : getRootNode().getChildren())
+        {
+            if (isSectionNode(node))
+            {
+                if (globalSection)
+                {
+                    sections.add(null);
+                    globalSection = false;
+                }
+                sections.add(node.getName());
+            }
+            else
+            {
+                globalSection = true;
+            }
+        }
+
+        return sections;
+    }
+
+    /**
+     * Loads the data of this {@code ConfigurationSource} from the specified
+     * reader.
+     *
+     * @param reader the reader
+     * @throws IOException if an I/O error occurs
+     * @throws ConfigurationException if the data cannot be parsed
+     */
+    public void load(Reader reader) throws IOException, ConfigurationException
+    {
+        BufferedReader bufferedReader = new BufferedReader(reader);
+        ConfigurationNode sectionNode = getRootNode();
+
+        String line = bufferedReader.readLine();
+        while (line != null)
+        {
+            line = line.trim();
+            if (!isCommentLine(line))
+            {
+                if (isSectionLine(line))
+                {
+                    String section = line.substring(1, line.length() - 1);
+                    sectionNode = getSectionNode(section);
+                }
+
+                else
+                {
+                    String key = "";
+                    String value = "";
+                    int index = line.indexOf("=");
+                    if (index >= 0)
+                    {
+                        key = line.substring(0, index);
+                        value = parseValue(line.substring(index + 1),
+                                bufferedReader);
+                    }
+                    else
+                    {
+                        index = line.indexOf(":");
+                        if (index >= 0)
+                        {
+                            key = line.substring(0, index);
+                            value = parseValue(line.substring(index + 1),
+                                    bufferedReader);
+                        }
+                        else
+                        {
+                            key = line;
+                        }
+                    }
+                    key = key.trim();
+                    if (key.length() < 1)
+                    {
+                        // use space for properties with no key
+                        key = " ";
+                    }
+                    getNodeHandler().addChild(sectionNode, key, value);
+                }
+            }
+
+            line = bufferedReader.readLine();
+        }
+    }
+
+    /**
+     * Writes the data of this {@code ConfigurationSource} into the specified
+     * writer. This creates a valid ini file.
+     *
+     * @param writer the writer
+     * @throws IOException if an I/O error occurs
+     * @throws ConfigurationException for all other errors
+     */
+    public void save(Writer writer) throws IOException, ConfigurationException
+    {
+        PrintWriter out = new PrintWriter(writer);
+
+        // First print the global section
+        boolean hasGlobal = false;
+        for (ConfigurationNode node : getRootNode().getChildren())
+        {
+            if (!isSectionNode(node))
+            {
+                printNode(out, node);
+                hasGlobal = true;
+            }
+        }
+        if (hasGlobal)
+        {
+            out.println(); // add an empty line
+        }
+
+        // Now process the remaining sections
+        for (String section : getSections())
+        {
+            if (section != null)
+            {
+                out.print("[");
+                out.print(section);
+                out.print("]");
+                out.println();
+
+                ConfigurationNode sectionNode = getSectionNode(section);
+                for (ConfigurationNode node : sectionNode.getChildren())
+                {
+                    printNode(out, node);
+                }
+
+                out.println();
+            }
+        }
+
+        out.flush();
+    }
+
+    /**
+     * Prints the specified node into the given writer. This method is called by
+     * {@code save()} for each processed node.
+     *
+     * @param out the writer
+     * @param node the node to be printed
+     */
+    protected void printNode(PrintWriter out, ConfigurationNode node)
+    {
+        out.print(node.getName());
+        out.print(" = ");
+        out.print(formatValue(String.valueOf(node.getValue())));
+        out.println();
+    }
+
+    /**
+     * Determine if the given line is a comment line.
+     *
+     * @param line The line to check.
+     * @return true if the line is empty or starts with one of the comment
+     *         characters
+     */
+    protected boolean isCommentLine(String line)
+    {
+        if (line == null)
+        {
+            return false;
+        }
+        // blank lines are also treated as comment lines
+        return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
+    }
+
+    /**
+     * Determine if the given line is a section.
+     *
+     * @param line The line to check.
+     * @return true if the line contains a section
+     */
+    protected boolean isSectionLine(String line)
+    {
+        if (line == null)
+        {
+            return false;
+        }
+        return line.startsWith("[") && line.endsWith("]");
+    }
+
+    /**
+     * Adds custom capabilities to the specified capabilities collection. This
+     * implementation adds a {@link LocatorSupport} capability.
+     *
+     * @param caps the capabilities collection
+     */
+    @Override
+    protected void appendCapabilities(Collection<Capability> caps)
+    {
+        caps.add(new Capability(LocatorSupport.class,
+                new DefaultLocatorSupport(this)));
+    }
+
+    /**
+     * Obtains the node representing the specified section. This method is
+     * called while the configuration is loaded. If a node for this section
+     * already exists, it is returned. Otherwise a new node is created.
+     *
+     * @param sectionName the name of the section
+     * @return the node for this section
+     */
+    private ConfigurationNode getSectionNode(String sectionName)
+    {
+        List<ConfigurationNode> nodes = getRootNode().getChildren(sectionName);
+        if (!nodes.isEmpty())
+        {
+            return nodes.get(0);
+        }
+
+        ConfigurationNode node = getNodeHandler().addChild(getRootNode(),
+                sectionName);
+        markSectionNode(node);
+        return node;
+    }
+
+    /**
+     * Parse the value to remove the quotes and ignoring the comment. Example:
+     *
+     * <pre>
+     * &quot;value&quot; ; comment -&gt; value
+     * </pre>
+     *
+     * <pre>
+     * 'value' ; comment -&gt; value
+     * </pre>
+     *
+     * @param val the value to be parsed
+     * @param reader the reader (needed if multiple lines have to be read)
+     * @throws IOException if an IO error occurs
+     */
+    private static String parseValue(String val, BufferedReader reader)
+            throws IOException
+    {
+        StringBuilder propertyValue = new StringBuilder();
+        boolean lineContinues;
+        String value = val.trim();
+
+        do
+        {
+            boolean quoted = value.startsWith("\"") || value.startsWith("'");
+            boolean stop = false;
+            boolean escape = false;
+
+            char quote = quoted ? value.charAt(0) : 0;
+
+            int i = quoted ? 1 : 0;
+
+            StringBuilder result = new StringBuilder();
+            while (i < value.length() && !stop)
+            {
+                char c = value.charAt(i);
+
+                if (quoted)
+                {
+                    if ('\\' == c && !escape)
+                    {
+                        escape = true;
+                    }
+                    else if (!escape && quote == c)
+                    {
+                        stop = true;
+                    }
+                    else if (escape && quote == c)
+                    {
+                        escape = false;
+                        result.append(c);
+                    }
+                    else
+                    {
+                        if (escape)
+                        {
+                            escape = false;
+                            result.append('\\');
+                        }
+
+                        result.append(c);
+                    }
+                }
+                else
+                {
+                    if (!isCommentChar(c))
+                    {
+                        result.append(c);
+                    }
+                    else
+                    {
+                        stop = true;
+                    }
+                }
+
+                i++;
+            }
+
+            String v = result.toString();
+            if (!quoted)
+            {
+                v = v.trim();
+                lineContinues = lineContinues(v);
+                if (lineContinues)
+                {
+                    // remove trailing "\"
+                    v = v.substring(0, v.length() - 1).trim();
+                }
+            }
+            else
+            {
+                lineContinues = lineContinues(value, i);
+            }
+            propertyValue.append(v);
+
+            if (lineContinues)
+            {
+                propertyValue.append(LINE_SEPARATOR);
+                value = reader.readLine();
+            }
+        } while (lineContinues && value != null);
+
+        return propertyValue.toString();
+    }
+
+    /**
+     * Tests whether the specified string contains a line continuation marker.
+     *
+     * @param line the string to check
+     * @return a flag whether this line continues
+     */
+    private static boolean lineContinues(String line)
+    {
+        String s = line.trim();
+        return s.equals(LINE_CONT)
+                || (s.length() > 2 && s.endsWith(LINE_CONT) && Character
+                        .isWhitespace(s.charAt(s.length() - 2)));
+    }
+
+    /**
+     * Tests whether the specified string contains a line continuation marker
+     * after the specified position. This method parses the string to remove a
+     * comment that might be present. Then it checks whether a line continuation
+     * marker can be found at the end.
+     *
+     * @param line the line to check
+     * @param pos the start position
+     * @return a flag whether this line continues
+     */
+    private static boolean lineContinues(String line, int pos)
+    {
+        String s;
+
+        if (pos >= line.length())
+        {
+            s = line;
+        }
+        else
+        {
+            int end = pos;
+            while (end < line.length() && !isCommentChar(line.charAt(end)))
+            {
+                end++;
+            }
+            s = line.substring(pos, end);
+        }
+
+        return lineContinues(s);
+    }
+
+    /**
+     * Add quotes around the specified value if it contains a comment character.
+     */
+    private String formatValue(String value)
+    {
+        boolean quoted = false;
+
+        for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
+        {
+            char c = COMMENT_CHARS.charAt(i);
+            if (value.indexOf(c) != -1)
+            {
+                quoted = true;
+            }
+        }
+
+        if (quoted)
+        {
+            return '"' + StringUtils.replace(value, "\"", "\\\"") + '"';
+        }
+        else
+        {
+            return value;
+        }
+    }
+
+    /**
+     * Tests whether the specified character is a comment character.
+     *
+     * @param c the character
+     * @return a flag whether this character starts a comment
+     */
+    private static boolean isCommentChar(char c)
+    {
+        return COMMENT_CHARS.indexOf(c) >= 0;
+    }
+
+    /**
+     * Marks a configuration node as a section node. This means that this node
+     * represents a section header. This implementation uses the node's
+     * reference property to store a flag.
+     *
+     * @param node the node to be marked
+     */
+    private static void markSectionNode(ConfigurationNode node)
+    {
+        node.setReference(Boolean.TRUE);
+    }
+
+    /**
+     * Checks whether the specified configuration node represents a section.
+     *
+     * @param node the node in question
+     * @return a flag whether this node represents a section
+     */
+    private static boolean isSectionNode(ConfigurationNode node)
+    {
+        return node.getReference() != null || node.getChildrenCount() > 0;
+    }
+}

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/base/impl/INIConfigurationSource.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java?rev=831560&view=auto
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java (added)
+++ commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java Sat Oct 31 16:11:38 2009
@@ -0,0 +1,659 @@
+/*
+ * 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.commons.configuration2.base.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.commons.configuration2.ConfigurationAssert;
+import org.apache.commons.configuration2.ConfigurationException;
+import org.apache.commons.configuration2.base.Configuration;
+import org.apache.commons.configuration2.base.ConfigurationImpl;
+import org.apache.commons.configuration2.base.LocatorSupport;
+import org.apache.commons.configuration2.fs.Locator;
+import org.apache.commons.configuration2.fs.URLLocator;
+import org.apache.commons.configuration2.tree.ConfigurationNode;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test class for {@code INIConfigurationSource}
+ *
+ * @author <a
+ *         href="http://commons.apache.org/configuration/team-list.html">Commons
+ *         Configuration team</a>
+ * @version $Id$
+ */
+public class TestINIConfigurationSource
+{
+    private static String LINE_SEPARATOR = System.getProperty("line.separator");
+
+    /** Constant for the content of an ini file. */
+    private static final String INI_DATA = "[section1]" + LINE_SEPARATOR
+            + "var1 = foo" + LINE_SEPARATOR + "var2 = 451" + LINE_SEPARATOR
+            + LINE_SEPARATOR + "[section2]" + LINE_SEPARATOR + "var1 = 123.45"
+            + LINE_SEPARATOR + "var2 = bar" + LINE_SEPARATOR + LINE_SEPARATOR
+            + "[section3]" + LINE_SEPARATOR + "var1 = true" + LINE_SEPARATOR
+            + "interpolated = ${section3.var1}" + LINE_SEPARATOR
+            + "multi = foo" + LINE_SEPARATOR + "multi = bar" + LINE_SEPARATOR
+            + LINE_SEPARATOR;
+
+    private static final String INI_DATA2 = "[section4]" + LINE_SEPARATOR
+            + "var1 = \"quoted value\"" + LINE_SEPARATOR
+            + "var2 = \"quoted value\\nwith \\\"quotes\\\"\"" + LINE_SEPARATOR
+            + "var3 = 123 ; comment" + LINE_SEPARATOR
+            + "var4 = \"1;2;3\" ; comment" + LINE_SEPARATOR
+            + "var5 = '\\'quoted\\' \"value\"' ; comment" + LINE_SEPARATOR
+            + "var6 = \"\"" + LINE_SEPARATOR;
+
+    private static final String INI_DATA3 = "[section5]" + LINE_SEPARATOR
+            + "multiLine = one \\" + LINE_SEPARATOR + "    two      \\"
+            + LINE_SEPARATOR + " three" + LINE_SEPARATOR
+            + "singleLine = C:\\Temp\\" + LINE_SEPARATOR
+            + "multiQuoted = one \\" + LINE_SEPARATOR + "\"  two  \" \\"
+            + LINE_SEPARATOR + "  three" + LINE_SEPARATOR
+            + "multiComment = one \\ ; a comment" + LINE_SEPARATOR + "two"
+            + LINE_SEPARATOR + "multiQuotedComment = \" one \" \\ ; comment"
+            + LINE_SEPARATOR + "two" + LINE_SEPARATOR + "noFirstLine = \\"
+            + LINE_SEPARATOR + "  line 2" + LINE_SEPARATOR
+            + "continueNoLine = one \\" + LINE_SEPARATOR;
+
+    /** An ini file with a global section. */
+    private static final String INI_DATA_GLOBAL = "globalVar = testGlobal"
+            + LINE_SEPARATOR + LINE_SEPARATOR + INI_DATA;
+
+    /** Constant for the name of the test output file. */
+    private static final String TEST_OUT_FILE = "test.ini";
+
+    /** A test ini file. */
+    private static File testFile;
+
+    /** The URL to the test file. */
+    private static URL testFileURL;
+
+    /** The source to be tested. */
+    private INIConfigurationSource source;
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception
+    {
+        testFile = ConfigurationAssert.getOutFile(TEST_OUT_FILE);
+        testFileURL = ConfigurationAssert.getOutURL(TEST_OUT_FILE);
+    }
+
+    @Before
+    public void setUp() throws Exception
+    {
+        source = new INIConfigurationSource();
+    }
+
+    @After
+    public void tearDown() throws Exception
+    {
+        if (testFile.exists())
+        {
+            assertTrue("Cannot remove test file: " + testFile, testFile
+                    .delete());
+        }
+    }
+
+    /**
+     * Populates the test INIConfigurationSource object with the given data.
+     *
+     * @param data the data of the configuration source (an ini file as string)
+     * @throws ConfigurationException if an error occurs
+     */
+    private void load(String data) throws ConfigurationException
+    {
+        StringReader reader = new StringReader(data);
+        try
+        {
+            source.load(reader);
+        }
+        catch (IOException ioex)
+        {
+            throw new ConfigurationException(ioex);
+        }
+        reader.close();
+    }
+
+    /**
+     * Creates a configuration with the test configuration source and
+     * initializes the source with the given data.
+     *
+     * @param data the data (an ini file as string)
+     * @return the configuration
+     * @throws ConfigurationException if an error occurs
+     */
+    private Configuration<ConfigurationNode> setUpConfig(String data)
+            throws ConfigurationException
+    {
+        load(data);
+        return new ConfigurationImpl<ConfigurationNode>(source);
+    }
+
+    /**
+     * Tests whether a source can be correctly saved.
+     */
+    @Test
+    public void testSave() throws Exception
+    {
+        Writer writer = new StringWriter();
+        Configuration<ConfigurationNode> config = new ConfigurationImpl<ConfigurationNode>(
+                source);
+        config.addProperty("section1.var1", "foo");
+        config.addProperty("section1.var2", "451");
+        config.addProperty("section2.var1", "123.45");
+        config.addProperty("section2.var2", "bar");
+        config.addProperty("section3.var1", "true");
+        config.addProperty("section3.interpolated", "${section3.var1}");
+        config.addProperty("section3.multi", "foo");
+        config.addProperty("section3.multi", "bar");
+        source.save(writer);
+
+        assertEquals("Wrong content of ini file", INI_DATA, writer.toString());
+    }
+
+    /**
+     * Tests whether a configuration source with a global section can be saved.
+     */
+    @Test
+    public void testSaveWithGlobalSection() throws ConfigurationException,
+            IOException
+    {
+        load(INI_DATA_GLOBAL);
+        StringWriter writer = new StringWriter();
+        source.save(writer);
+        assertEquals("Wrong content of ini file", INI_DATA_GLOBAL, writer
+                .toString());
+    }
+
+    /**
+     * Tests whether the configuration contains the expected data.
+     *
+     * @param config the configuration to check
+     */
+    private void checkContent(Configuration<ConfigurationNode> config)
+    {
+        assertEquals("Wrong string 1", "foo", config.getString("section1.var1"));
+        assertEquals("Wrong int", 451, config.getInt("section1.var2"));
+        assertEquals("Wrong double", 123.45, config.getDouble("section2.var1"),
+                .001);
+        assertEquals("Wrong string 2", "bar", config.getString("section2.var2"));
+        assertTrue("Wrong boolean", config.getBoolean("section3.var1"));
+        assertEquals("Wrong number of sections", 3, source.getSections().size());
+    }
+
+    /**
+     * Helper method for testing the load operation. Loads the specified content
+     * into a configuration and then checks some properties.
+     *
+     * @param data the data to load
+     */
+    private void checkLoad(String data) throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(data);
+        checkContent(config);
+    }
+
+    /**
+     * Tests whether an ini file can be parsed.
+     */
+    @Test
+    public void testLoad() throws ConfigurationException
+    {
+        checkLoad(INI_DATA);
+    }
+
+    /**
+     * Tests the load() method if the alternative value separator is used (a ':'
+     * for '=').
+     */
+    @Test
+    public void testLoadAlternativeSeparator() throws ConfigurationException
+    {
+        checkLoad(INI_DATA.replace('=', ':'));
+    }
+
+    /**
+     * Test whether comment lines are correctly detected.
+     */
+    @Test
+    public void testIsCommentLine()
+    {
+        assertTrue("# not detected", source.isCommentLine("#comment1"));
+        assertTrue("; not detected", source.isCommentLine(";comment1"));
+        assertFalse("Wrong comment", source.isCommentLine("nocomment=true"));
+        assertFalse("Null detected as comment", source.isCommentLine(null));
+    }
+
+    /**
+     * Test whether section declarations are correctly detected.
+     */
+    @Test
+    public void testIsSectionLine()
+    {
+        assertTrue("Not a section", source.isSectionLine("[section]"));
+        assertFalse("A section", source.isSectionLine("nosection=true"));
+        assertFalse("Null a section", source.isSectionLine(null));
+    }
+
+    /**
+     * Tests whether sections are correctly extracted.
+     */
+    @Test
+    public void testGetSections()
+    {
+        Configuration<ConfigurationNode> config = new ConfigurationImpl<ConfigurationNode>(
+                source);
+        config.addProperty("test1.foo", "bar");
+        config.addProperty("test2.foo", "abc");
+        Set<String> expResult = new HashSet<String>();
+        expResult.add("test1");
+        expResult.add("test2");
+        Set<String> result = source.getSections();
+        assertEquals("Wrong set with sections", expResult, result);
+    }
+
+    @Test
+    public void testQuotedValue() throws Exception
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA2);
+        assertEquals("value", "quoted value", config.getString("section4.var1"));
+    }
+
+    @Test
+    public void testQuotedValueWithQuotes() throws Exception
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA2);
+        assertEquals("value", "quoted value\\nwith \"quotes\"", config
+                .getString("section4.var2"));
+    }
+
+    @Test
+    public void testValueWithComment() throws Exception
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA2);
+        assertEquals("value", "123", config.getString("section4.var3"));
+    }
+
+    @Test
+    public void testQuotedValueWithComment() throws Exception
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA2);
+        assertEquals("value", "1;2;3", config.getString("section4.var4"));
+    }
+
+    @Test
+    public void testQuotedValueWithSingleQuotes() throws Exception
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA2);
+        assertEquals("value", "'quoted' \"value\"", config
+                .getString("section4.var5"));
+    }
+
+    @Test
+    public void testWriteValueWithCommentChar() throws Exception
+    {
+        Configuration<ConfigurationNode> config = new ConfigurationImpl<ConfigurationNode>(
+                source);
+        config.setProperty("section.key1", "1;2;3");
+
+        StringWriter writer = new StringWriter();
+        source.save(writer);
+        source = new INIConfigurationSource();
+
+        Configuration<ConfigurationNode> config2 = setUpConfig(writer
+                .toString());
+        assertEquals("value", "1;2;3", config2.getString("section.key1"));
+    }
+
+    /**
+     * Tests whether whitespace is left unchanged for quoted values.
+     */
+    @Test
+    public void testQuotedValueWithWhitespace() throws Exception
+    {
+        final String content = "CmdPrompt = \" [test@cmd ~]$ \"";
+        Configuration<ConfigurationNode> config = setUpConfig(content);
+        assertEquals("Wrong propert value", " [test@cmd ~]$ ", config
+                .getString("CmdPrompt"));
+    }
+
+    /**
+     * Tests a quoted value with space and a comment.
+     */
+    @Test
+    public void testQuotedValueWithWhitespaceAndComment() throws Exception
+    {
+        final String content = "CmdPrompt = \" [test@cmd ~]$ \" ; a comment";
+        Configuration<ConfigurationNode> config = setUpConfig(content);
+        assertEquals("Wrong propert value", " [test@cmd ~]$ ", config
+                .getString("CmdPrompt"));
+    }
+
+    /**
+     * Tests an empty quoted value.
+     */
+    @Test
+    public void testQuotedValueEmpty() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA2);
+        assertEquals("Wrong value for empty property", "", config
+                .getString("section4.var6"));
+    }
+
+    /**
+     * Tests a property that has no value.
+     */
+    @Test
+    public void testGetPropertyNoValue() throws ConfigurationException
+    {
+        final String data = INI_DATA2 + LINE_SEPARATOR + "noValue ="
+                + LINE_SEPARATOR;
+        Configuration<ConfigurationNode> config = setUpConfig(data);
+        assertEquals("Wrong value of key", "", config
+                .getString("section4.noValue"));
+    }
+
+    /**
+     * Tests a property that has no key.
+     */
+    @Test
+    public void testGetPropertyNoKey() throws ConfigurationException
+    {
+        final String data = INI_DATA2 + LINE_SEPARATOR + "= noKey"
+                + LINE_SEPARATOR;
+        Configuration<ConfigurationNode> config = setUpConfig(data);
+        assertEquals("Cannot find property with no key", "noKey", config
+                .getString("section4. "));
+    }
+
+    /**
+     * Tests reading a property from the global section.
+     */
+    @Test
+    public void testGlobalProperty() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA_GLOBAL);
+        assertEquals("Wrong value of global property", "testGlobal", config
+                .getString("globalVar"));
+    }
+
+    /**
+     * Tests whether the test configuration source contains exactly the expected
+     * sections.
+     *
+     * @param expected an array with the expected sections
+     */
+    private void checkSectionNames(String[] expected)
+    {
+        Set<String> sectionNames = source.getSections();
+        Iterator<String> it = sectionNames.iterator();
+        for (int idx = 0; idx < expected.length; idx++)
+        {
+            assertEquals("Wrong section at " + idx, expected[idx], it.next());
+        }
+        assertFalse("Too many sections", it.hasNext());
+    }
+
+    /**
+     * Tests the names of the sections returned by the configuration source.
+     *
+     * @param data the data of the ini configuration source
+     * @param expected the expected section names
+     * @return a configuration wrapping the test source
+     */
+    private Configuration<ConfigurationNode> checkSectionNames(String data,
+            String[] expected) throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(data);
+        checkSectionNames(expected);
+        return config;
+    }
+
+    /**
+     * Tests querying the sections if a global section if available.
+     */
+    @Test
+    public void testGetSectionsWithGlobal() throws ConfigurationException
+    {
+        checkSectionNames(INI_DATA_GLOBAL, new String[] {
+                null, "section1", "section2", "section3"
+        });
+    }
+
+    /**
+     * Tests querying the sections if there is no global section.
+     */
+    @Test
+    public void testGetSectionsNoGlobal() throws ConfigurationException
+    {
+        checkSectionNames(INI_DATA, new String[] {
+                "section1", "section2", "section3"
+        });
+    }
+
+    /**
+     * Tests whether variables containing a dot are not misinterpreted as
+     * sections. This test is related to CONFIGURATION-327.
+     */
+    @Test
+    public void testGetSectionsDottedVar() throws ConfigurationException
+    {
+        final String data = "dotted.var = 1" + LINE_SEPARATOR + INI_DATA_GLOBAL;
+        Configuration<ConfigurationNode> config = checkSectionNames(data,
+                new String[] {
+                        null, "section1", "section2", "section3"
+                });
+        assertEquals("Wrong value of dotted variable", 1, config
+                .getInt("dotted..var"));
+    }
+
+    /**
+     * Tests whether a section added later is also found by getSections().
+     */
+    @Test
+    public void testGetSectionsAdded() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA2);
+        config.addProperty("section5.test", Boolean.TRUE);
+        checkSectionNames(new String[] {
+                "section4", "section5"
+        });
+    }
+
+    /**
+     * Tests whether a sub configuration with the content of a section can be
+     * queried.
+     */
+    @Test
+    public void testGetSubConfigurationForSection()
+            throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA);
+        Configuration<ConfigurationNode> section = config
+                .configurationAt("section1");
+        assertEquals("Wrong value of var1", "foo", section.getString("var1"));
+        assertEquals("Wrong value of var2", "451", section.getString("var2"));
+    }
+
+    /**
+     * Tests a property whose value spans multiple lines.
+     */
+    @Test
+    public void testLineContinuation() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA3);
+        assertEquals("Wrong value", "one" + LINE_SEPARATOR + "two"
+                + LINE_SEPARATOR + "three", config
+                .getString("section5.multiLine"));
+    }
+
+    /**
+     * Tests a property value that ends on a backslash, which is no line
+     * continuation character.
+     */
+    @Test
+    public void testLineContinuationNone() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA3);
+        assertEquals("Wrong value", "C:\\Temp\\", config
+                .getString("section5.singleLine"));
+    }
+
+    /**
+     * Tests a property whose value spans multiple lines when quoting is
+     * involved. In this case whitespace must not be trimmed.
+     */
+    @Test
+    public void testLineContinuationQuoted() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA3);
+        assertEquals("Wrong value", "one" + LINE_SEPARATOR + "  two  "
+                + LINE_SEPARATOR + "three", config
+                .getString("section5.multiQuoted"));
+    }
+
+    /**
+     * Tests a property whose value spans multiple lines with a comment.
+     */
+    @Test
+    public void testLineContinuationComment() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA3);
+        assertEquals("Wrong value", "one" + LINE_SEPARATOR + "two", config
+                .getString("section5.multiComment"));
+    }
+
+    /**
+     * Tests a property with a quoted value spanning multiple lines and a
+     * comment.
+     */
+    @Test
+    public void testLineContinuationQuotedComment()
+            throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA3);
+        assertEquals("Wrong value", " one " + LINE_SEPARATOR + "two", config
+                .getString("section5.multiQuotedComment"));
+    }
+
+    /**
+     * Tests a multi-line property value with an empty line.
+     */
+    @Test
+    public void testLineContinuationEmptyLine() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA3);
+        assertEquals("Wrong value", LINE_SEPARATOR + "line 2", config
+                .getString("section5.noFirstLine"));
+    }
+
+    /**
+     * Tests a line continuation at the end of the file.
+     */
+    @Test
+    public void testLineContinuationAtEnd() throws ConfigurationException
+    {
+        Configuration<ConfigurationNode> config = setUpConfig(INI_DATA3);
+        assertEquals("Wrong value", "one" + LINE_SEPARATOR, config
+                .getString("section5.continueNoLine"));
+    }
+
+    /**
+     * Writes a test ini file.
+     *
+     * @param content the content of the file
+     * @throws IOException if an error occurs
+     */
+    private static void writeTestFile(String content) throws IOException
+    {
+        PrintWriter out = new PrintWriter(new FileWriter(testFile));
+        try
+        {
+            out.println(content);
+        }
+        finally
+        {
+            out.close();
+        }
+    }
+
+    /**
+     * Tests whether the LocatorSupport capability is provided.
+     */
+    @Test
+    public void testLocatorSupport()
+    {
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        assertNotNull("No locator support", locSupport);
+        assertNull("Got already a locator", locSupport.getLocator());
+    }
+
+    /**
+     * Tests whether the configuration source can be loaded from a locator.
+     */
+    @Test
+    public void testLoadFromLocator() throws IOException,
+            ConfigurationException
+    {
+        writeTestFile(INI_DATA);
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.setLocator(new URLLocator(testFileURL));
+        locSupport.load();
+        Configuration<ConfigurationNode> config = new ConfigurationImpl<ConfigurationNode>(
+                source);
+        checkContent(config);
+    }
+
+    /**
+     * Tests whether the source can save its data to a locator.
+     */
+    @Test
+    public void testSaveToLocator() throws IOException, ConfigurationException
+    {
+        Locator loc = new URLLocator(testFileURL);
+        load(INI_DATA);
+        LocatorSupport locSupport = source.getCapability(LocatorSupport.class);
+        locSupport.save(loc);
+        INIConfigurationSource source2 = new INIConfigurationSource();
+        LocatorSupport locSupport2 = source2
+                .getCapability(LocatorSupport.class);
+        locSupport2.load(loc);
+        Configuration<ConfigurationNode> config = new ConfigurationImpl<ConfigurationNode>(
+                source2);
+        checkContent(config);
+    }
+}

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/base/impl/TestINIConfigurationSource.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain