Author: oheger
Date: Sat Jul 22 10:02:38 2006
New Revision: 424604

URL: http://svn.apache.org/viewvc?rev=424604&view=rev
Log:
Introduced new PropertiesConfigurationLayout class for preserving the structure 
of properties files

Added:
    
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
   (with props)
Modified:
    
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java

Modified: 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java
URL: 
http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java?rev=424604&r1=424603&r2=424604&view=diff
==============================================================================
--- 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java
 (original)
+++ 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java
 Sat Jul 22 10:02:38 2006
@@ -16,16 +16,14 @@
 
 package org.apache.commons.configuration;
 
-import java.io.BufferedReader;
 import java.io.File;
 import java.io.FilterWriter;
 import java.io.IOException;
 import java.io.LineNumberReader;
 import java.io.Reader;
-import java.io.StringReader;
 import java.io.Writer;
 import java.net.URL;
-import java.util.Date;
+import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
@@ -134,6 +132,14 @@
  *      second.prop = ${first.prop}/second
  * </pre>
  *
+ * <p>A <code>PropertiesConfiguration</code> object is associated with an
+ * instance of the <code>[EMAIL PROTECTED] 
PropertiesConfigurationLayout}</code> class,
+ * which is responsible for storing the layout of the parsed properties file
+ * (i.e. empty lines, comments, and such things). The <code>getLayout()</code>
+ * method can be used to obtain this layout object. With 
<code>setLayout()</code>
+ * a new layout object can be set. This should be done before a properties file
+ * was loaded.
+ *
  * @see java.util.Properties#load
  *
  * @author <a href="mailto:[EMAIL PROTECTED]">Stefano Mazzocchi</a>
@@ -175,18 +181,21 @@
     /** Constant for the platform specific line separator.*/
     private static final String LINE_SEPARATOR = 
System.getProperty("line.separator");
 
+    /** Constant for the supported comment characters.*/
+    static final String COMMENT_CHARS = "#!";
+
     /** Constant for the radix of hex numbers.*/
     private static final int HEX_RADIX = 16;
 
     /** Constant for the length of a unicode literal.*/
     private static final int UNICODE_LEN = 4;
 
+    /** Stores the layout object.*/
+    private PropertiesConfigurationLayout layout;
+
     /** Allow file inclusion or not */
     private boolean includesAllowed;
 
-    /** Comment header of the .properties file */
-    private String header;
-
     // initialization block to set the encoding before loading the file in the 
constructors
     {
         setEncoding(DEFAULT_ENCODING);
@@ -293,7 +302,7 @@
      */
     public String getHeader()
     {
-        return header;
+        return getLayout().getHeaderComment();
     }
 
     /**
@@ -304,7 +313,47 @@
      */
     public void setHeader(String header)
     {
-        this.header = header;
+        getLayout().setHeaderComment(header);
+    }
+
+    /**
+     * Returns the associated layout object.
+     *
+     * @return the associated layout object
+     * @since 1.3
+     */
+    public synchronized PropertiesConfigurationLayout getLayout()
+    {
+        if (layout == null)
+        {
+            layout = createLayout();
+        }
+        return layout;
+    }
+
+    /**
+     * Sets the associated layout object.
+     *
+     * @param layout the new layout object; can be <b>null</b>, then a new
+     * layout object will be created
+     * @since 1.3
+     */
+    public void setLayout(PropertiesConfigurationLayout layout)
+    {
+        this.layout = layout;
+    }
+
+    /**
+     * Creates the associated layout object. This method is invoked when the
+     * layout object is accessed and has not been created yet. Derived classes
+     * can override this method to hook in a different layout implementation.
+     *
+     * @return the layout object to use
+     * @since 1.3
+     */
+    protected PropertiesConfigurationLayout createLayout()
+    {
+        return new PropertiesConfigurationLayout(this);
     }
 
     /**
@@ -319,60 +368,12 @@
      */
     public synchronized void load(Reader in) throws ConfigurationException
     {
-        PropertiesReader reader = new PropertiesReader(in);
         boolean oldAutoSave = isAutoSave();
         setAutoSave(false);
 
         try
         {
-            while (true)
-            {
-                String line = reader.readProperty();
-
-                if (line == null)
-                {
-                    break; // EOF
-                }
-
-                // parse the line
-                String[] property = parseProperty(line);
-                String key = property[0];
-                String value = property[1];
-
-                // Though some software (e.g. autoconf) may produce
-                // empty values like foo=\n, emulate the behavior of
-                // java.util.Properties by setting the value to the
-                // empty string.
-
-                if (StringUtils.isNotEmpty(getInclude()) && 
key.equalsIgnoreCase(getInclude()))
-                {
-                    if (getIncludesAllowed())
-                    {
-                        String [] files;
-                        if (!isDelimiterParsingDisabled())
-                        {
-                            files = StringUtils.split(value, 
getListDelimiter());
-                        }
-                        else
-                        {
-                            files = new String[]{value};
-                        }
-                        for (int i = 0; i < files.length; i++)
-                        {
-                            loadIncludeFile(files[i].trim());
-                        }
-                    }
-                }
-                else
-                {
-                    addProperty(StringEscapeUtils.unescapeJava(key), 
unescapeJava(value, getListDelimiter()));
-                }
-
-            }
-        }
-        catch (IOException ioe)
-        {
-            throw new ConfigurationException("Could not load configuration 
from input stream.", ioe);
+            getLayout().load(in);
         }
         finally
         {
@@ -391,71 +392,114 @@
         enterNoReload();
         try
         {
-            PropertiesWriter out = new PropertiesWriter(writer, 
getListDelimiter());
+            getLayout().save(writer);
+        }
+        finally
+        {
+            exitNoReload();
+        }
+    }
 
-            if (header != null)
-            {
-                BufferedReader reader = new BufferedReader(new 
StringReader(header));
-                String line;
-                while ((line = reader.readLine()) != null)
-                {
-                    out.writeComment(line);
-                }
-                out.writeln(null);
-            }
+    /**
+     * Extend the setBasePath method to turn includes
+     * on and off based on the existence of a base path.
+     *
+     * @param basePath The new basePath to set.
+     */
+    public void setBasePath(String basePath)
+    {
+        super.setBasePath(basePath);
+        setIncludesAllowed(StringUtils.isNotEmpty(basePath));
+    }
 
-            out.writeComment("written by PropertiesConfiguration");
-            out.writeComment(new Date().toString());
-            out.writeln(null);
+    /**
+     * This method is invoked by the associated
+     * <code>[EMAIL PROTECTED] PropertiesConfigurationLayout}</code> object 
for each
+     * property definition detected in the parsed properties file. Its task is
+     * to check whether this is a special property definition (e.g. the
+     * <code>include</code> property). If not, the property must be added to
+     * this configuration. The return value indicates whether the property
+     * should be treated as a normal property. If it is <b>false</b>, the
+     * layout object will ignore this property.
+     *
+     * @param key the property key
+     * @param value the property value
+     * @return a flag whether this is a normal property
+     * @throws ConfigurationException if an error occurs
+     * @since 1.3
+     */
+    boolean propertyLoaded(String key, String value)
+            throws ConfigurationException
+    {
+        boolean result;
 
-            Iterator keys = getKeys();
-            while (keys.hasNext())
+        if (StringUtils.isNotEmpty(getInclude())
+                && key.equalsIgnoreCase(getInclude()))
+        {
+            if (getIncludesAllowed())
             {
-                String key = (String) keys.next();
-                Object value = getProperty(key);
-
-                if (value instanceof List)
+                String[] files;
+                if (!isDelimiterParsingDisabled())
                 {
-                    out.writeProperty(key, (List) value);
+                    files = StringUtils.split(value, getListDelimiter());
                 }
                 else
                 {
-                    out.writeProperty(key, value);
+                    files = new String[]
+                    { value };
+                }
+                for (int i = 0; i < files.length; i++)
+                {
+                    loadIncludeFile(files[i].trim());
                 }
             }
-
-            out.flush();
-        }
-        catch (IOException e)
-        {
-            throw new ConfigurationException(e.getMessage(), e);
+            result = false;
         }
-        finally
+
+        else
         {
-            exitNoReload();
+            addProperty(key, value);
+            result = true;
         }
+
+        return result;
     }
 
     /**
-     * Extend the setBasePath method to turn includes
-     * on and off based on the existence of a base path.
+     * Tests whether a line is a comment, i.e. whether it starts with a comment
+     * character.
      *
-     * @param basePath The new basePath to set.
+     * @param line the line
+     * @return a flag if this is a comment line
+     * @since 1.3
      */
-    public void setBasePath(String basePath)
+    static boolean isCommentLine(String line)
     {
-        super.setBasePath(basePath);
-        setIncludesAllowed(StringUtils.isNotEmpty(basePath));
+        String s = line.trim();
+        // blanc lines are also treated as comment lines
+        return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
     }
 
     /**
-     * This class is used to read properties lines.  These lines do
+     * This class is used to read properties lines. These lines do
      * not terminate with new-line chars but rather when there is no
      * backslash sign a the end of the line.  This is used to
      * concatenate multiple lines for readability.
      */
     public static class PropertiesReader extends LineNumberReader
     {
+        /** Stores the comment lines for the currently processed property.*/
+        private List commentLines;
+
+        /** Stores the name of the last read property.*/
+        private String propertyName;
+
+        /** Stores the value of the last read property.*/
+        private String propertyValue;
+
+        /** Stores the list delimiter character.*/
+        private char delimiter;
+
         /**
          * Constructor.
          *
@@ -463,13 +507,30 @@
          */
         public PropertiesReader(Reader reader)
         {
+            this(reader, AbstractConfiguration.getDefaultListDelimiter());
+        }
+
+        /**
+         * Creates a new instance of <code>PropertiesReader</code> and sets
+         * the underlaying reader and the list delimiter.
+         *
+         * @param reader the reader
+         * @param listDelimiter the list delimiter character
+         * @since 1.3
+         */
+        public PropertiesReader(Reader reader, char listDelimiter)
+        {
             super(reader);
+            commentLines = new ArrayList();
+            delimiter = listDelimiter;
         }
 
         /**
-         * Read a property. Returns null if Stream is
+         * Reads a property line. Returns null if Stream is
          * at EOF. Concatenates lines ending with "\".
          * Skips lines beginning with "#" or "!" and empty lines.
+         * The return value is a property definition (<code>&lt;name&gt;</code>
+         * = <code>&lt;value&gt;</code>)
          *
          * @return A string containing a property value or null
          *
@@ -477,6 +538,7 @@
          */
         public String readProperty() throws IOException
         {
+            commentLines.clear();
             StringBuffer buffer = new StringBuffer();
 
             while (true)
@@ -488,14 +550,14 @@
                     return null;
                 }
 
-                line = line.trim();
-
-                // skip comments and empty lines
-                if (StringUtils.isEmpty(line) || (line.charAt(0) == '#') || 
(line.charAt(0) == '!'))
+                if (isCommentLine(line))
                 {
+                    commentLines.add(line);
                     continue;
                 }
 
+                line = line.trim();
+
                 if (checkCombineLines(line))
                 {
                     line = line.substring(0, line.length() - 1);
@@ -511,6 +573,71 @@
         }
 
         /**
+         * Parses the next property from the input stream and stores the found
+         * name and value in internal fields. These fields can be obtained 
using
+         * the provided getter methods. The return value indicates whether EOF
+         * was reached (<b>false</b>) or whether further properties are
+         * available (<b>true</b>).
+         *
+         * @return a flag if further properties are available
+         * @throws IOException if an error occurs
+         * @since 1.3
+         */
+        public boolean nextProperty() throws IOException
+        {
+            String line = readProperty();
+
+            if (line == null)
+            {
+                return false; // EOF
+            }
+
+            // parse the line
+            String[] property = parseProperty(line);
+            propertyName = StringEscapeUtils.unescapeJava(property[0]);
+            propertyValue = unescapeJava(property[1], delimiter);
+            return true;
+        }
+
+        /**
+         * Returns the comment lines that have been read for the last property.
+         *
+         * @return the comment lines for the last property returned by
+         * <code>readProperty()</code>
+         * @since 1.3
+         */
+        public List getCommentLines()
+        {
+            return commentLines;
+        }
+
+        /**
+         * Returns the name of the last read property. This method can be 
called
+         * after <code>[EMAIL PROTECTED] #nextProperty()}</code> was invoked 
and its
+         * return value was <b>true</b>.
+         *
+         * @return the name of the last read property
+         * @since 1.3
+         */
+        public String getPropertyName()
+        {
+            return propertyName;
+        }
+
+        /**
+         * Returns the value of the last read property. This method can be
+         * called after <code>[EMAIL PROTECTED] #nextProperty()}</code> was 
invoked and
+         * its return value was <b>true</b>.
+         *
+         * @return the value of the last read property
+         * @since 1.3
+         */
+        public String getPropertyValue()
+        {
+            return propertyValue;
+        }
+
+        /**
          * Checks if the passed in line should be combined with the following.
          * This is true, if the line ends with an odd number of backslashes.
          *
@@ -527,6 +654,109 @@
 
             return bsCount % 2 == 1;
         }
+
+        /**
+         * Parse a property line and return the key and the value in an array.
+         *
+         * @param line the line to parse
+         * @return an array with the property's key and value
+         * @since 1.2
+         */
+        private static String[] parseProperty(String line)
+        {
+            // sorry for this spaghetti code, please replace it as soon as
+            // possible with a regexp when the Java 1.3 requirement is dropped
+
+            String[] result = new String[2];
+            StringBuffer key = new StringBuffer();
+            StringBuffer value = new StringBuffer();
+
+            // state of the automaton:
+            // 0: key parsing
+            // 1: antislash found while parsing the key
+            // 2: separator crossing
+            // 3: value parsing
+            int state = 0;
+
+            for (int pos = 0; pos < line.length(); pos++)
+            {
+                char c = line.charAt(pos);
+
+                switch (state)
+                {
+                    case 0:
+                        if (c == '\\')
+                        {
+                            state = 1;
+                        }
+                        else if (ArrayUtils.contains(WHITE_SPACE, c))
+                        {
+                            // switch to the separator crossing state
+                            state = 2;
+                        }
+                        else if (ArrayUtils.contains(SEPARATORS, c))
+                        {
+                            // switch to the value parsing state
+                            state = 3;
+                        }
+                        else
+                        {
+                            key.append(c);
+                        }
+
+                        break;
+
+                    case 1:
+                        if (ArrayUtils.contains(SEPARATORS, c) || 
ArrayUtils.contains(WHITE_SPACE, c))
+                        {
+                            // this is an escaped separator or white space
+                            key.append(c);
+                        }
+                        else
+                        {
+                            // another escaped character, the '\' is preserved
+                            key.append('\\');
+                            key.append(c);
+                        }
+
+                        // return to the key parsing state
+                        state = 0;
+
+                        break;
+
+                    case 2:
+                        if (ArrayUtils.contains(WHITE_SPACE, c))
+                        {
+                            // do nothing, eat all white spaces
+                            state = 2;
+                        }
+                        else if (ArrayUtils.contains(SEPARATORS, c))
+                        {
+                            // switch to the value parsing state
+                            state = 3;
+                        }
+                        else
+                        {
+                            // any other character indicates we encoutered the 
beginning of the value
+                            value.append(c);
+
+                            // switch to the value parsing state
+                            state = 3;
+                        }
+
+                        break;
+
+                    case 3:
+                        value.append(c);
+                        break;
+                }
+            }
+
+            result[0] = key.toString().trim();
+            result[1] = value.toString().trim();
+
+            return result;
+        }
     } // class PropertiesReader
 
     /**
@@ -559,16 +789,7 @@
          */
         public void writeProperty(String key, Object value) throws IOException
         {
-            write(escapeKey(key));
-            write(" = ");
-            if (value != null)
-            {
-                String v = StringEscapeUtils.escapeJava(String.valueOf(value));
-                v = StringUtils.replace(v, String.valueOf(delimiter), "\\" + 
delimiter);
-                write(v);
-            }
-
-            writeln(null);
+            writeProperty(key, value, false);
         }
 
         /**
@@ -588,6 +809,48 @@
         }
 
         /**
+         * Writes the given property and its value. If the value happens to be 
a
+         * list, the <code>forceSingleLine</code> flag is evaluated. If it is
+         * set, all values are written on a single line using the list 
delimiter
+         * as separator.
+         *
+         * @param key the property key
+         * @param value the property value
+         * @param forceSingleLine the &quot;force single line&quot; flag
+         * @throws IOException if an error occurs
+         * @since 1.3
+         */
+        public void writeProperty(String key, Object value,
+                boolean forceSingleLine) throws IOException
+        {
+            String v;
+
+            if (value instanceof List)
+            {
+                List values = (List) value;
+                if (forceSingleLine)
+                {
+                    v = makeSingleLineValue(values);
+                }
+                else
+                {
+                    writeProperty(key, values);
+                    return;
+                }
+            }
+            else
+            {
+                v = escapeValue(value);
+            }
+
+            write(escapeKey(key));
+            write(" = ");
+            write(v);
+
+            writeln(null);
+        }
+
+        /**
          * Write a comment.
          *
          * @param comment the comment to write
@@ -629,13 +892,55 @@
         }
 
         /**
+         * Escapes the given property value. Delimiter characters in the value
+         * will be escaped.
+         *
+         * @param value the property value
+         * @return the escaped property value
+         * @since 1.3
+         */
+        private String escapeValue(Object value)
+        {
+            String v = StringEscapeUtils.escapeJava(String.valueOf(value));
+            return StringUtils.replace(v, String.valueOf(delimiter), "\\"
+                    + delimiter);
+        }
+
+        /**
+         * Transforms a list of values into a single line value.
+         *
+         * @param values the list with the values
+         * @return a string with the single line value (can be <b>null</b>)
+         * @since 1.3
+         */
+        private String makeSingleLineValue(List values)
+        {
+            if (!values.isEmpty())
+            {
+                Iterator it = values.iterator();
+                StringBuffer buf = new StringBuffer(escapeValue(it.next()));
+                while (it.hasNext())
+                {
+                    buf.append(delimiter);
+                    buf.append(escapeValue(it.next()));
+                }
+                return buf.toString();
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+        /**
          * Helper method for writing a line with the platform specific line
          * ending.
          *
          * @param s the content of the line (may be <b>null</b>)
          * @throws IOException if an error occurs
+         * @since 1.3
          */
-        private void writeln(String s) throws IOException
+        public void writeln(String s) throws IOException
         {
             if (s != null)
             {
@@ -766,109 +1071,6 @@
         }
 
         return out.toString();
-    }
-
-    /**
-     * Parse a property line and return the key and the value in an array.
-     *
-     * @param line the line to parse
-     * @return an array with the property's key and value
-     * @since 1.2
-     */
-    private String[] parseProperty(String line)
-    {
-        // sorry for this spaghetti code, please replace it as soon as
-        // possible with a regexp when the Java 1.3 requirement is dropped
-
-        String[] result = new String[2];
-        StringBuffer key = new StringBuffer();
-        StringBuffer value = new StringBuffer();
-
-        // state of the automaton:
-        // 0: key parsing
-        // 1: antislash found while parsing the key
-        // 2: separator crossing
-        // 3: value parsing
-        int state = 0;
-
-        for (int pos = 0; pos < line.length(); pos++)
-        {
-            char c = line.charAt(pos);
-
-            switch (state)
-            {
-                case 0:
-                    if (c == '\\')
-                    {
-                        state = 1;
-                    }
-                    else if (ArrayUtils.contains(WHITE_SPACE, c))
-                    {
-                        // switch to the separator crossing state
-                        state = 2;
-                    }
-                    else if (ArrayUtils.contains(SEPARATORS, c))
-                    {
-                        // switch to the value parsing state
-                        state = 3;
-                    }
-                    else
-                    {
-                        key.append(c);
-                    }
-
-                    break;
-
-                case 1:
-                    if (ArrayUtils.contains(SEPARATORS, c) || 
ArrayUtils.contains(WHITE_SPACE, c))
-                    {
-                        // this is an escaped separator or white space
-                        key.append(c);
-                    }
-                    else
-                    {
-                        // another escaped character, the '\' is preserved
-                        key.append('\\');
-                        key.append(c);
-                    }
-
-                    // return to the key parsing state
-                    state = 0;
-
-                    break;
-
-                case 2:
-                    if (ArrayUtils.contains(WHITE_SPACE, c))
-                    {
-                        // do nothing, eat all white spaces
-                        state = 2;
-                    }
-                    else if (ArrayUtils.contains(SEPARATORS, c))
-                    {
-                        // switch to the value parsing state
-                        state = 3;
-                    }
-                    else
-                    {
-                        // any other character indicates we encoutered the 
beginning of the value
-                        value.append(c);
-
-                        // switch to the value parsing state
-                        state = 3;
-                    }
-
-                    break;
-
-                case 3:
-                    value.append(c);
-                    break;
-            }
-        }
-
-        result[0] = key.toString().trim();
-        result[1] = value.toString().trim();
-
-        return result;
     }
 
     /**

Added: 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
URL: 
http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java?rev=424604&view=auto
==============================================================================
--- 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
 (added)
+++ 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
 Sat Jul 22 10:02:38 2006
@@ -0,0 +1,802 @@
+/*
+ * Copyright 2001-2006 The Apache Software Foundation.
+ *
+ * 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 org.apache.commons.configuration;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.collections.map.LinkedMap;
+import org.apache.commons.configuration.event.ConfigurationEvent;
+import org.apache.commons.configuration.event.ConfigurationListener;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * <p>
+ * A helper class used by <code>[EMAIL PROTECTED] 
PropertiesConfiguration}</code> to keep
+ * the layout of a properties file.
+ * </p>
+ * <p>
+ * Instances of this class are associated with a
+ * <code>PropertiesConfiguration</code> object. They are responsible for
+ * analyzing properties files and for extracting as much information about the
+ * file layout (e.g. empty lines, comments) as possible. When the properties
+ * file is written back again it should be close to the original.
+ * </p>
+ * <p>
+ * The <code>PropertiesConfigurationLayout</code> object associated with a
+ * <code>PropertiesConfiguration</code> object can be obtained using the
+ * <code>getLayout()</code> method of the configuration. Then the methods
+ * provided by this class can be used to alter the properties file's layout.
+ * </p>
+ * <p>
+ * Implementation note: This is a very simple implementation, which is far away
+ * from being perfect, i.e. the original layout of a properties file won't be
+ * reproduced in all cases. One limitation is that comments for multi-valued
+ * property keys are concatenated. Maybe this implementation can later be
+ * improved.
+ * </p>
+ * <p>
+ * To get an impression how this class works consider the following properties
+ * file:
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * # A demo configuration file
+ * # for Demo App 1.42
+ *
+ * # Application name
+ * AppName=Demo App
+ *
+ * # Application vendor
+ * AppVendor=DemoSoft
+ *
+ *
+ * # GUI properties
+ * # Window Color
+ * windowColors=0xFFFFFF,0x000000
+ *
+ * # Include some setting
+ * include=settings.properties
+ * # Another vendor
+ * AppVendor=TestSoft
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * For this example the following points are relevant:
+ * </p>
+ * <p>
+ * <ul>
+ * <li>The first two lines are set as header comment. The header comment is
+ * determined by the last blanc line before the first property definition.</li>
+ * <li>For the property <code>AppName</code> one comment line and one
+ * leading blanc line is stored.</li>
+ * <li>For the property <code>windowColors</code> two comment lines and two
+ * leading blanc lines are stored.</li>
+ * <li>Include files is something this class cannot deal with well. When saving
+ * the properties configuration back, the included properties are simply
+ * contained in the original file. The comment before the include property is
+ * skipped.</li>
+ * <li>For all properties except for <code>AppVendor</code> the &quot;single
+ * line&quot; flag is set. This is relevant only for <code>windowColors</code>,
+ * which has multiple values defined in one line using the separator 
character.</li>
+ * <li>The <code>AppVendor</code> property appears twice. The comment lines
+ * are concatenated, so that <code>layout.getComment("AppVendor");</code> will
+ * result in <code>Application vendor&lt;CR&gt;Another vendor</code>, whith
+ * <code>&lt;CR&gt;</code> meaning the line separator. In addition the
+ * &quot;single line&quot; flag is set to <b>false</b> for this property. When
+ * the file is saved, two property definitions will be written (in 
series).</li>
+ * </ul>
+ * </p>
+ *
+ * @author <a
+ * 
href="http://jakarta.apache.org/commons/configuration/team-list.html";>Commons
+ * Configuration team</a>
+ * @version $Id$
+ * @since 1.3
+ */
+public class PropertiesConfigurationLayout implements ConfigurationListener
+{
+    /** Constant for the line break character. */
+    private static final String CR = System.getProperty("line.separator");
+
+    /** Constant for the default comment prefix. */
+    private static final String COMMENT_PREFIX = "# ";
+
+    /** Stores the associated configuration object. */
+    private PropertiesConfiguration configuration;
+
+    /** Stores a map with the contained layout information. */
+    private Map layoutData;
+
+    /** Stores the header comment. */
+    private String headerComment;
+
+    /** A counter for determining nested load calls. */
+    private int loadCounter;
+
+    /** Stores the force single line flag. */
+    private boolean forceSingleLine;
+
+    /**
+     * Creates a new instance of <code>PropertiesConfigurationLayout</code>
+     * and initializes it with the associated configuration object.
+     *
+     * @param config the configuration (must not be <b>null</b>)
+     */
+    public PropertiesConfigurationLayout(PropertiesConfiguration config)
+    {
+        if (config == null)
+        {
+            throw new IllegalArgumentException(
+                    "Configuration must not be null!");
+        }
+        configuration = config;
+        layoutData = new LinkedMap();
+        config.addConfigurationListener(this);
+    }
+
+    /**
+     * Returns the associated configuration object.
+     *
+     * @return the associated configuration
+     */
+    public PropertiesConfiguration getConfiguration()
+    {
+        return configuration;
+    }
+
+    /**
+     * Returns the comment for the specified property key in a cononical form.
+     * &quot;Canonical&quot; means that either all lines start with a comment
+     * character or none. The <code>commentChar</code> parameter is 
<b>false</b>,
+     * all comment characters are removed, so that the result is only the plain
+     * text of the comment. Otherwise it is ensured that each line of the
+     * comment starts with a comment character.
+     *
+     * @param key the key of the property
+     * @param commentChar determines whether all lines should start with 
comment
+     * characters or not
+     * @return the canonical comment for this key (can be <b>null</b>)
+     */
+    public String getCanonicalComment(String key, boolean commentChar)
+    {
+        String comment = getComment(key);
+        if (comment == null)
+        {
+            return null;
+        }
+        else
+        {
+            return trimComment(comment, commentChar);
+        }
+    }
+
+    /**
+     * Returns the comment for the specified property key. The comment is
+     * returned as it was set (either manually by calling
+     * <code>setComment()</code> or when it was loaded from a properties
+     * file). No modifications are performed.
+     *
+     * @param key the key of the property
+     * @return the comment for this key (can be <b>null</b>)
+     */
+    public String getComment(String key)
+    {
+        return fetchLayoutData(key).getComment();
+    }
+
+    /**
+     * Sets the comment for the specified property key. The comment (or its
+     * single lines if it is a multi-line comment) can start with a comment
+     * character. If this is the case, it will be written without changes.
+     * Otherwise a default comment character is added automatically.
+     *
+     * @param key the key of the property
+     * @param comment the comment for this key (can be <b>null</b>, then the
+     * comment will be removed)
+     */
+    public void setComment(String key, String comment)
+    {
+        fetchLayoutData(key).setComment(comment);
+    }
+
+    /**
+     * Returns the number of blanc lines before this property key. If this key
+     * does not exist, 0 will be returned.
+     *
+     * @param key the property key
+     * @return the number of blanc lines before the property definition for 
this
+     * key
+     */
+    public int getBlancLinesBefore(String key)
+    {
+        return fetchLayoutData(key).getBlancLines();
+    }
+
+    /**
+     * Sets the number of blanc lines before the given property key. This can 
be
+     * used for a logical grouping of properties.
+     *
+     * @param key the property key
+     * @param number the number of blanc lines to add before this property
+     * definition
+     */
+    public void setBlancLinesBefore(String key, int number)
+    {
+        fetchLayoutData(key).setBlancLines(number);
+    }
+
+    /**
+     * Returns the header comment of the represented properties file in a
+     * canonical form. With the <code>commentChar</code> parameter it can be
+     * specified whether comment characters should be stripped or be always
+     * present.
+     *
+     * @param commentChar determines the presence of comment characters
+     * @return the header comment (can be <b>null</b>)
+     */
+    public String getCanonicalHeaderComment(boolean commentChar)
+    {
+        return (getHeaderComment() == null) ? null : trimComment(
+                getHeaderComment(), commentChar);
+    }
+
+    /**
+     * Returns the header comment of the represented properties file. This
+     * method returns the header comment exactly as it was set using
+     * <code>setHeaderComment()</code> or extracted from the loaded properties
+     * file.
+     *
+     * @return the header comment (can be <b>null</b>)
+     */
+    public String getHeaderComment()
+    {
+        return headerComment;
+    }
+
+    /**
+     * Sets the header comment for the represented properties file. This 
comment
+     * will be output on top of the file.
+     *
+     * @param comment the comment
+     */
+    public void setHeaderComment(String comment)
+    {
+        headerComment = comment;
+    }
+
+    /**
+     * Returns a flag whether the specified property is defined on a single
+     * line. This is meaningful only if this property has multiple values.
+     *
+     * @param key the property key
+     * @return a flag if this property is defined on a single line
+     */
+    public boolean isSingleLine(String key)
+    {
+        return fetchLayoutData(key).isSingleLine();
+    }
+
+    /**
+     * Sets the &quot;single line flag&quot; for the specified property key.
+     * This flag is evaluated if the property has multiple values (i.e. if it 
is
+     * a list property). In this case, if the flag is set, all values will be
+     * written in a single property definition using the list delimiter as
+     * separator. Otherwise multiple lines will be written for this property,
+     * each line containing one property value.
+     *
+     * @param key the property key
+     * @param f the single line flag
+     */
+    public void setSingleLine(String key, boolean f)
+    {
+        fetchLayoutData(key).setSingleLine(f);
+    }
+
+    /**
+     * Returns the &quot;force single line&quot; flag.
+     *
+     * @return the force single line flag
+     * @see #setForceSingleLine(boolean)
+     */
+    public boolean isForceSingleLine()
+    {
+        return forceSingleLine;
+    }
+
+    /**
+     * Sets the &quot;force single line&quot; flag. If this flag is set, all
+     * properties with multiple values are written on single lines. This mode
+     * provides more compatibility with <code>java.lang.Properties</code>,
+     * which cannot deal with multiple definitions of a single property.
+     *
+     * @param f the force single line flag
+     */
+    public void setForceSingleLine(boolean f)
+    {
+        forceSingleLine = f;
+    }
+
+    /**
+     * Returns a set with all property keys managed by this object.
+     *
+     * @return a set with all contained property keys
+     */
+    public Set getKeys()
+    {
+        return layoutData.keySet();
+    }
+
+    /**
+     * Reads a properties file and stores its internal structure. The found
+     * properties will be added to the associated configuration object.
+     *
+     * @param in the reader to the properties file
+     * @throws ConfigurationException if an error occurs
+     */
+    public void load(Reader in) throws ConfigurationException
+    {
+        if (++loadCounter == 1)
+        {
+            getConfiguration().removeConfigurationListener(this);
+        }
+        PropertiesConfiguration.PropertiesReader reader = new 
PropertiesConfiguration.PropertiesReader(
+                in, getConfiguration().getListDelimiter());
+
+        try
+        {
+            while (reader.nextProperty())
+            {
+                if (getConfiguration().propertyLoaded(reader.getPropertyName(),
+                        reader.getPropertyValue()))
+                {
+                    boolean contained = layoutData.containsKey(reader
+                            .getPropertyName());
+                    int blancLines = 0;
+                    int idx = checkHeaderComment(reader.getCommentLines());
+                    while (idx < reader.getCommentLines().size()
+                            && ((String) reader.getCommentLines().get(idx))
+                                    .length() < 1)
+                    {
+                        idx++;
+                        blancLines++;
+                    }
+                    String comment = extractComment(reader.getCommentLines(),
+                            idx, reader.getCommentLines().size() - 1);
+                    PropertyLayoutData data = fetchLayoutData(reader
+                            .getPropertyName());
+                    if (contained)
+                    {
+                        data.addComment(comment);
+                        data.setSingleLine(false);
+                    }
+                    else
+                    {
+                        data.setComment(comment);
+                        data.setBlancLines(blancLines);
+                    }
+                }
+            }
+        }
+        catch (IOException ioex)
+        {
+            throw new ConfigurationException(ioex);
+        }
+        finally
+        {
+            if (--loadCounter == 0)
+            {
+                getConfiguration().addConfigurationListener(this);
+            }
+        }
+    }
+
+    /**
+     * Writes the properties file to the given writer, preserving as much of 
its
+     * structure as possible.
+     *
+     * @param out the writer
+     * @throws ConfigurationException if an error occurs
+     */
+    public void save(Writer out) throws ConfigurationException
+    {
+        try
+        {
+            PropertiesConfiguration.PropertiesWriter writer = new 
PropertiesConfiguration.PropertiesWriter(
+                    out, getConfiguration().getListDelimiter());
+            if (headerComment != null)
+            {
+                writer.writeln(getCanonicalHeaderComment(true));
+                writer.writeln(null);
+            }
+
+            for (Iterator it = layoutData.keySet().iterator(); it.hasNext();)
+            {
+                String key = (String) it.next();
+                if (getConfiguration().containsKey(key))
+                {
+
+                    // Output blanc lines before property
+                    for (int i = 0; i < getBlancLinesBefore(key); i++)
+                    {
+                        writer.writeln(null);
+                    }
+
+                    // Output the comment
+                    if (getComment(key) != null)
+                    {
+                        writer.writeln(getCanonicalComment(key, true));
+                    }
+
+                    // Output the property and its value
+                    writer.writeProperty(key, getConfiguration().getProperty(
+                            key), isForceSingleLine() || isSingleLine(key));
+                }
+            }
+            writer.flush();
+        }
+        catch (IOException ioex)
+        {
+            throw new ConfigurationException(ioex);
+        }
+    }
+
+    /**
+     * The event listener callback. Here event notifications of the
+     * configuration object are processed to update the layout object properly.
+     *
+     * @param event the event object
+     */
+    public void configurationChanged(ConfigurationEvent event)
+    {
+        if (event.isBeforeUpdate())
+        {
+            if (AbstractFileConfiguration.EVENT_RELOAD == event.getType())
+            {
+                clear();
+            }
+        }
+
+        else
+        {
+            switch (event.getType())
+            {
+            case AbstractConfiguration.EVENT_ADD_PROPERTY:
+                boolean contained = layoutData.containsKey(event
+                        .getPropertyName());
+                PropertyLayoutData data = fetchLayoutData(event
+                        .getPropertyName());
+                data.setSingleLine(!contained);
+                break;
+            case AbstractConfiguration.EVENT_CLEAR_PROPERTY:
+                layoutData.remove(event.getPropertyName());
+                break;
+            case AbstractConfiguration.EVENT_CLEAR:
+                clear();
+                break;
+            case AbstractConfiguration.EVENT_SET_PROPERTY:
+                fetchLayoutData(event.getPropertyName());
+                break;
+            }
+        }
+    }
+
+    /**
+     * Returns a layout data object for the specified key. If this is a new 
key,
+     * a new object is created and initialized with default values.
+     *
+     * @param key the key
+     * @return the corresponding layout data object
+     */
+    private PropertyLayoutData fetchLayoutData(String key)
+    {
+        if (key == null)
+        {
+            throw new IllegalArgumentException("Property key must not be 
null!");
+        }
+
+        PropertyLayoutData data = (PropertyLayoutData) layoutData.get(key);
+        if (data == null)
+        {
+            data = new PropertyLayoutData();
+            data.setSingleLine(true);
+            layoutData.put(key, data);
+        }
+
+        return data;
+    }
+
+    /**
+     * Removes all content from this layout object.
+     */
+    private void clear()
+    {
+        layoutData.clear();
+        setHeaderComment(null);
+    }
+
+    /**
+     * Tests whether a line is a comment, i.e. whether it starts with a comment
+     * character.
+     *
+     * @param line the line
+     * @return a flag if this is a comment line
+     */
+    static boolean isCommentLine(String line)
+    {
+        return PropertiesConfiguration.isCommentLine(line);
+    }
+
+    /**
+     * Trims a comment. This method either removes all comment characters from
+     * the given string, leaving only the plain comment text or ensures that
+     * every line starts with a valid comment character.
+     *
+     * @param s the string to be processed
+     * @param comment if <b>true</b>, a comment character will always be
+     * enforced; if <b>false</b>, it will be removed
+     * @return the trimmed comment
+     */
+    static String trimComment(String s, boolean comment)
+    {
+        StringBuffer buf = new StringBuffer(s.length());
+        int lastPos = 0;
+        int pos;
+
+        do
+        {
+            pos = s.indexOf(CR, lastPos);
+            if (pos >= 0)
+            {
+                String line = s.substring(lastPos, pos);
+                buf.append(stripCommentChar(line, comment)).append(CR);
+                lastPos = pos + CR.length();
+            }
+        } while (pos >= 0);
+
+        if (lastPos < s.length())
+        {
+            buf.append(stripCommentChar(s.substring(lastPos), comment));
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Either removes the comment character from the given comment line or
+     * ensures that the line starts with a comment character.
+     *
+     * @param s the comment line
+     * @param comment if <b>true</b>, a comment character will always be
+     * enforced; if <b>false</b>, it will be removed
+     * @return the line without comment character
+     */
+    static String stripCommentChar(String s, boolean comment)
+    {
+        if (s.length() < 1 || (isCommentLine(s) == comment))
+        {
+            return s;
+        }
+
+        else
+        {
+            if (!comment)
+            {
+                int pos = 0;
+                // find first comment character
+                while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s
+                        .charAt(pos)) < 0)
+                {
+                    pos++;
+                }
+
+                // Remove leading spaces
+                pos++;
+                while (pos < s.length()
+                        && Character.isWhitespace(s.charAt(pos)))
+                {
+                    pos++;
+                }
+
+                return (pos < s.length()) ? s.substring(pos)
+                        : StringUtils.EMPTY;
+            }
+            else
+            {
+                return COMMENT_PREFIX + s;
+            }
+        }
+    }
+
+    /**
+     * Extracts a comment string from the given range of the specified comment
+     * lines. The single lines are added using a line feed as separator.
+     *
+     * @param commentLines a list with comment lines
+     * @param from the start index
+     * @param to the end index (inclusive)
+     * @return the comment string (<b>null</b> if it is undefined)
+     */
+    private String extractComment(List commentLines, int from, int to)
+    {
+        if (to < from)
+        {
+            return null;
+        }
+
+        else
+        {
+            StringBuffer buf = new StringBuffer((String) 
commentLines.get(from));
+            for (int i = from + 1; i <= to; i++)
+            {
+                buf.append(CR);
+                buf.append(commentLines.get(i));
+            }
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Checks if parts of the passed in comment can be used as header comment.
+     * This method checks whether a header comment can be defined (i.e. whether
+     * this is the first comment in the loaded file). If this is the case, it 
is
+     * searched for the lates blanc line. This line will mark the end of the
+     * header comment. The return value is the index of the first line in the
+     * passed in list, which does not belong to the header comment.
+     *
+     * @param commentLines the comment lines
+     * @return the index of the next line after the header comment
+     */
+    private int checkHeaderComment(List commentLines)
+    {
+        if (loadCounter == 1 && getHeaderComment() == null
+                && layoutData.isEmpty())
+        {
+            // This is the first comment. Search for blanc lines.
+            int index = commentLines.size() - 1;
+            while (index >= 0
+                    && ((String) commentLines.get(index)).length() > 0)
+            {
+                index--;
+            }
+            setHeaderComment(extractComment(commentLines, 0, index - 1));
+            return index + 1;
+        }
+        else
+        {
+            return 0;
+        }
+    }
+
+    /**
+     * A helper class for storing all layout related information for a
+     * configuration property.
+     */
+    static class PropertyLayoutData
+    {
+        /** Stores the comment for the property. */
+        private StringBuffer comment;
+
+        /** Stores the number of blanc lines before this property. */
+        private int blancLines;
+
+        /** Stores the single line property. */
+        private boolean singleLine;
+
+        /**
+         * Creates a new instance of <code>PropertyLayoutData</code>.
+         */
+        public PropertyLayoutData()
+        {
+            singleLine = true;
+        }
+
+        /**
+         * Returns the number of blanc lines before this property.
+         *
+         * @return the number of blanc lines before this property
+         */
+        public int getBlancLines()
+        {
+            return blancLines;
+        }
+
+        /**
+         * Sets the number of properties before this property.
+         *
+         * @param blancLines the number of properties before this property
+         */
+        public void setBlancLines(int blancLines)
+        {
+            this.blancLines = blancLines;
+        }
+
+        /**
+         * Returns the single line flag.
+         *
+         * @return the single line flag
+         */
+        public boolean isSingleLine()
+        {
+            return singleLine;
+        }
+
+        /**
+         * Sets the single line flag.
+         *
+         * @param singleLine the single line flag
+         */
+        public void setSingleLine(boolean singleLine)
+        {
+            this.singleLine = singleLine;
+        }
+
+        /**
+         * Adds a comment for this property. If already a comment exists, the
+         * new comment is added (separated by a newline).
+         *
+         * @param s the comment to add
+         */
+        public void addComment(String s)
+        {
+            if (s != null)
+            {
+                if (comment == null)
+                {
+                    comment = new StringBuffer(s);
+                }
+                else
+                {
+                    comment.append(CR).append(s);
+                }
+            }
+        }
+
+        /**
+         * Sets the comment for this property.
+         *
+         * @param s the new comment (can be <b>null</b>)
+         */
+        public void setComment(String s)
+        {
+            if (s == null)
+            {
+                comment = null;
+            }
+            else
+            {
+                comment = new StringBuffer(s);
+            }
+        }
+
+        /**
+         * Returns the comment for this property. The comment is returned as it
+         * is, without processing of comment characters.
+         *
+         * @return the comment (can be <b>null</b>)
+         */
+        public String getComment()
+        {
+            return (comment == null) ? null : comment.toString();
+        }
+    }
+}

Propchange: 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: 
jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain



---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to