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/03/21 21:18:56 UTC

svn commit: r757004 - in /commons/proper/configuration/branches/configuration2_experimental: src/main/java/org/apache/commons/configuration2/ src/test/java/org/apache/commons/configuration2/ xdocs/ xdocs/userguide/

Author: oheger
Date: Sat Mar 21 20:18:55 2009
New Revision: 757004

URL: http://svn.apache.org/viewvc?rev=757004&view=rev
Log:
CONFIGURATION-370: Ported changes to configuration2 branch.

Modified:
    commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
    commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java
    commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
    commons/proper/configuration/branches/configuration2_experimental/xdocs/changes.xml
    commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/howto_properties.xml
    commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/user_guide.xml

Modified: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java?rev=757004&r1=757003&r2=757004&view=diff
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java (original)
+++ commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java Sat Mar 21 20:18:55 2009
@@ -180,6 +180,12 @@
     static final String COMMENT_CHARS = "#!";
 
     /**
+     * Constant for the default <code>IOFactory</code>. This instance is used
+     * when no specific factory was set.
+     */
+    private static final IOFactory DEFAULT_IO_FACTORY = new DefaultIOFactory();
+
+    /**
      * This is the name of the property that can point to other
      * properties file for including other properties files.
      */
@@ -212,6 +218,9 @@
     /** Stores the layout object.*/
     private PropertiesConfigurationLayout layout;
 
+    /** The IOFactory for creating readers and writers.*/
+    private volatile IOFactory ioFactory;
+
     /** Allow file inclusion or not */
     private boolean includesAllowed;
 
@@ -405,6 +414,43 @@
     }
 
     /**
+     * Returns the <code>IOFactory</code> to be used for creating readers and
+     * writers when loading or saving this configuration.
+     *
+     * @return the <code>IOFactory</code>
+     * @since 1.7
+     */
+    public IOFactory getIOFactory()
+    {
+        return (ioFactory != null) ? ioFactory : DEFAULT_IO_FACTORY;
+    }
+
+    /**
+     * Sets the <code>IOFactory</code> to be used for creating readers and
+     * writers when loading or saving this configuration. Using this method a
+     * client can customize the reader and writer classes used by the load and
+     * save operations. Note that this method must be called before invoking
+     * one of the <code>load()</code> and <code>save()</code> methods.
+     * Especially, if you want to use a custom <code>IOFactory</code> for
+     * changing the <code>PropertiesReader</code>, you cannot load the
+     * configuration data in the constructor.
+     *
+     * @param ioFactory the new <code>IOFactory</code> (must not be <b>null</b>)
+     * @throws IllegalArgumentException if the <code>IOFactory</code> is
+     *         <b>null</b>
+     * @since 1.7
+     */
+    public void setIOFactory(IOFactory ioFactory)
+    {
+        if (ioFactory == null)
+        {
+            throw new IllegalArgumentException("IOFactory must not be null!");
+        }
+
+        this.ioFactory = ioFactory;
+    }
+
+    /**
      * Load the properties from the given reader.
      * Note that the <code>clear()</code> method is not called, so
      * the properties contained in the loaded file will be added to the
@@ -661,9 +707,7 @@
             }
 
             // parse the line
-            String[] property = parseProperty(line);
-            propertyName = StringEscapeUtils.unescapeJava(property[0]);
-            propertyValue = unescapeJava(property[1], delimiter);
+            parseProperty(line);
             return true;
         }
 
@@ -706,6 +750,51 @@
         }
 
         /**
+         * Parses a line read from the properties file. This method is called
+         * for each non-comment line read from the source file. Its task is to
+         * split the passed in line into the property key and its value. The
+         * results of the parse operation can be stored by calling the
+         * <code>initPropertyXXX()</code> methods.
+         *
+         * @param line the line read from the properties file
+         * @since 1.7
+         */
+        protected void parseProperty(String line)
+        {
+            String[] property = doParseProperty(line);
+            initPropertyName(property[0]);
+            initPropertyValue(property[1]);
+        }
+
+        /**
+         * Sets the name of the current property. This method can be called by
+         * <code>parseProperty()</code> for storing the results of the parse
+         * operation. It also ensures that the property key is correctly
+         * escaped.
+         *
+         * @param name the name of the current property
+         * @since 1.7
+         */
+        protected void initPropertyName(String name)
+        {
+            propertyName = StringEscapeUtils.unescapeJava(name);
+        }
+
+        /**
+         * Sets the value of the current property. This method can be called by
+         * <code>parseProperty()</code> for storing the results of the parse
+         * operation. It also ensures that the property value is correctly
+         * escaped.
+         *
+         * @param value the value of the current property
+         * @since 1.7
+         */
+        protected void initPropertyValue(String value)
+        {
+            propertyValue = unescapeJava(value, delimiter);
+        }
+
+        /**
          * 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.
          *
@@ -728,9 +817,8 @@
          *
          * @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)
+        private static String[] doParseProperty(String line)
         {
             Matcher matcher = PROPERTY_PATTERN.matcher(line);
 
@@ -950,6 +1038,82 @@
     } // class PropertiesWriter
 
     /**
+     * <p>
+     * Definition of an interface that allows customization of read and write
+     * operations.
+     * </p>
+     * <p>
+     * For reading and writing properties files the inner classes
+     * <code>PropertiesReader</code> and <code>PropertiesWriter</code> are used.
+     * This interface defines factory methods for creating both a
+     * <code>PropertiesReader</code> and a <code>PropertiesWriter</code>. An
+     * object implementing this interface can be passed to the
+     * <code>setIOFactory()</code> method of
+     * <code>PropertiesConfiguration</code>. Every time the configuration is
+     * read or written the <code>IOFactory</code> is asked to create the
+     * appropriate reader or writer object. This provides an opportunity to
+     * inject custom reader or writer implementations.
+     * </p>
+     *
+     * @since 1.7
+     */
+    public static interface IOFactory
+    {
+        /**
+         * Creates a <code>PropertiesReader</code> for reading a properties
+         * file. This method is called whenever the
+         * <code>PropertiesConfiguration</code> is loaded. The reader returned
+         * by this method is then used for parsing the properties file.
+         *
+         * @param in the underlying reader (of the properties file)
+         * @param delimiter the delimiter character for list parsing
+         * @return the <code>PropertiesReader</code> for loading the
+         *         configuration
+         */
+        PropertiesReader createPropertiesReader(Reader in, char delimiter);
+
+        /**
+         * Creates a <code>PropertiesWriter</code> for writing a properties
+         * file. This method is called before the
+         * <code>PropertiesConfiguration</code> is saved. The writer returned by
+         * this method is then used for writing the properties file.
+         *
+         * @param out the underlying writer (to the properties file)
+         * @param delimiter the delimiter character for list parsing
+         * @return the <code>PropertiesWriter</code> for saving the
+         *         configuration
+         */
+        PropertiesWriter createPropertiesWriter(Writer out, char delimiter);
+    }
+
+    /**
+     * <p>
+     * A default implementation of the <code>IOFactory</code> interface.
+     * </p>
+     * <p>
+     * This class implements the <code>createXXXX()</code> methods defined by
+     * the <code>IOFactory</code> interface in a way that the default objects
+     * (i.e. <code>PropertiesReader</code> and <code>PropertiesWriter</code> are
+     * returned. Customizing either the reader or the writer (or both) can be
+     * done by extending this class and overriding the corresponding
+     * <code>createXXXX()</code> method.
+     * </p>
+     */
+    public static class DefaultIOFactory implements IOFactory
+    {
+        public PropertiesReader createPropertiesReader(Reader in, char delimiter)
+        {
+            return new PropertiesReader(in, delimiter);
+        }
+
+        public PropertiesWriter createPropertiesWriter(Writer out,
+                char delimiter)
+        {
+            return new PropertiesWriter(out, delimiter);
+        }
+    }
+
+    /**
      * <p>Unescapes any Java literals found in the <code>String</code> to a
      * <code>Writer</code>.</p> This is a slightly modified version of the
      * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't

Modified: commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java?rev=757004&r1=757003&r2=757004&view=diff
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java (original)
+++ commons/proper/configuration/branches/configuration2_experimental/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java Sat Mar 21 20:18:55 2009
@@ -381,8 +381,9 @@
         {
             getConfiguration().removeConfigurationListener(this);
         }
-        PropertiesConfiguration.PropertiesReader reader = new PropertiesConfiguration.PropertiesReader(
-                in, getConfiguration().getListDelimiter());
+        PropertiesConfiguration.PropertiesReader reader = getConfiguration()
+        .getIOFactory().createPropertiesReader(in,
+                getConfiguration().getListDelimiter());
 
         try
         {
@@ -445,8 +446,8 @@
         {
             char delimiter = getConfiguration().isDelimiterParsingDisabled() ? 0
                     : getConfiguration().getListDelimiter();
-            PropertiesConfiguration.PropertiesWriter writer = new PropertiesConfiguration.PropertiesWriter(
-                    out, delimiter);
+            PropertiesConfiguration.PropertiesWriter writer = getConfiguration()
+                    .getIOFactory().createPropertiesWriter(out, delimiter);
             if (headerComment != null)
             {
                 writer.writeln(getCanonicalHeaderComment(true));
@@ -843,6 +844,7 @@
          *
          * @return the copy
          */
+        @Override
         public Object clone()
         {
             try

Modified: commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java?rev=757004&r1=757003&r2=757004&view=diff
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java (original)
+++ commons/proper/configuration/branches/configuration2_experimental/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java Sat Mar 21 20:18:55 2009
@@ -27,6 +27,7 @@
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.io.Writer;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLConnection;
@@ -47,14 +48,21 @@
  */
 public class TestPropertiesConfiguration extends TestCase
 {
+    /** Constant for a test property name.*/
+    private static final String PROP_NAME = "testProperty";
+
+    /** Constant for a test property value.*/
+    private static final String PROP_VALUE = "value";
+
+    /** The configuration to be tested.*/
     private PropertiesConfiguration conf;
 
     /** The File that we test with */
-    private String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
+    private static String testProperties = ConfigurationAssert.getTestFile("test.properties").getAbsolutePath();
 
-    private String testBasePath = ConfigurationAssert.TEST_DIR.getAbsolutePath();
-    private String testBasePath2 = ConfigurationAssert.TEST_DIR.getAbsoluteFile().getParentFile().getAbsolutePath();
-    private File testSavePropertiesFile = ConfigurationAssert.getOutFile("testsave.properties");
+    private static String testBasePath = ConfigurationAssert.TEST_DIR.getAbsolutePath();
+    private static String testBasePath2 = ConfigurationAssert.TEST_DIR.getAbsoluteFile().getParentFile().getAbsolutePath();
+    private static File testSavePropertiesFile = ConfigurationAssert.getOutFile("testsave.properties");
 
     @Override
     protected void setUp() throws Exception
@@ -429,12 +437,7 @@
             throws ConfigurationException
     {
         PropertiesConfiguration checkConfig = new PropertiesConfiguration(testSavePropertiesFile);
-        for (Iterator<String> i = conf.getKeys(); i.hasNext();)
-        {
-            String key = i.next();
-            assertTrue("The saved configuration doesn't contain the key '" + key + "'", checkConfig.containsKey(key));
-            assertEquals("Value of the '" + key + "' property", conf.getProperty(key), checkConfig.getProperty(key));
-        }
+        ConfigurationAssert.assertEquals(conf, checkConfig);
         return checkConfig;
     }
 
@@ -854,6 +857,91 @@
     }
 
     /**
+     * Tests whether a default IOFactory is set.
+     */
+    public void testGetIOFactoryDefault()
+    {
+        assertNotNull("No default IO factory", conf.getIOFactory());
+    }
+
+    /**
+     * Tests setting the IOFactory to null. This should cause an exception.
+     */
+    public void testSetIOFactoryNull()
+    {
+        try
+        {
+            conf.setIOFactory(null);
+            fail("Could set IO factory to null!");
+        }
+        catch (IllegalArgumentException iex)
+        {
+            // ok
+        }
+    }
+
+    /**
+     * Tests setting an IOFactory that uses a specialized reader.
+     */
+    public void testSetIOFactoryReader() throws ConfigurationException
+    {
+        final int propertyCount = 10;
+        conf.clear();
+        conf.setIOFactory(new PropertiesConfiguration.IOFactory()
+        {
+            public PropertiesConfiguration.PropertiesReader createPropertiesReader(
+                    Reader in, char delimiter)
+            {
+                return new PropertiesReaderTestImpl(in, delimiter,
+                        propertyCount);
+            }
+
+            public PropertiesConfiguration.PropertiesWriter createPropertiesWriter(
+                    Writer out, char delimiter)
+            {
+                throw new UnsupportedOperationException("Unexpected call!");
+            }
+        });
+        conf.load();
+        for (int i = 1; i <= propertyCount; i++)
+        {
+            assertEquals("Wrong property value at " + i, PROP_VALUE + i, conf
+                    .getString(PROP_NAME + i));
+        }
+    }
+
+    /**
+     * Tests setting an IOFactory that uses a specialized writer.
+     */
+    public void testSetIOFactoryWriter() throws ConfigurationException
+    {
+        conf.setIOFactory(new PropertiesConfiguration.IOFactory()
+        {
+            public PropertiesConfiguration.PropertiesReader createPropertiesReader(
+                    Reader in, char delimiter)
+            {
+                throw new UnsupportedOperationException("Unexpected call!");
+            }
+
+            public PropertiesConfiguration.PropertiesWriter createPropertiesWriter(
+                    Writer out, char delimiter)
+            {
+                try
+                {
+                    return new PropertiesWriterTestImpl(out, delimiter);
+                }
+                catch (IOException ioex)
+                {
+                    fail("Unexpected exception: " + ioex);
+                    return null;
+                }
+            }
+        });
+        conf.save(new StringWriter());
+        checkSavedConfig();
+    }
+
+    /**
      * Creates a configuration that can be used for testing copy operations.
      *
      * @return the configuration to be copied
@@ -991,4 +1079,61 @@
             return connection;
         }
     }
+
+    /**
+     * A test PropertiesReader for testing whether a custom reader can be
+     * injected. This implementation creates a configurable number of synthetic
+     * test properties.
+     */
+    private static class PropertiesReaderTestImpl extends
+            PropertiesConfiguration.PropertiesReader
+    {
+        /** The number of test properties to be created. */
+        private final int maxProperties;
+
+        /** The current number of properties. */
+        private int propertyCount;
+
+        public PropertiesReaderTestImpl(Reader reader, char listDelimiter,
+                int maxProps)
+        {
+            super(reader, listDelimiter);
+            assertEquals("Wrong list delimiter", ',', listDelimiter);
+            maxProperties = maxProps;
+        }
+
+        @Override
+        public String getPropertyName()
+        {
+            return PROP_NAME + propertyCount;
+        }
+
+        @Override
+        public String getPropertyValue()
+        {
+            return PROP_VALUE + propertyCount;
+        }
+
+        @Override
+        public boolean nextProperty() throws IOException
+        {
+            propertyCount++;
+            return propertyCount <= maxProperties;
+        }
+    }
+
+    /**
+     * A test PropertiesWriter for testing whether a custom writer can be
+     * injected. This implementation simply redirects all output into a test
+     * file.
+     */
+    private static class PropertiesWriterTestImpl extends
+            PropertiesConfiguration.PropertiesWriter
+    {
+        public PropertiesWriterTestImpl(Writer writer, char delimiter)
+                throws IOException
+        {
+            super(new FileWriter(testSavePropertiesFile), delimiter);
+        }
+    }
 }

Modified: commons/proper/configuration/branches/configuration2_experimental/xdocs/changes.xml
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/xdocs/changes.xml?rev=757004&r1=757003&r2=757004&view=diff
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/xdocs/changes.xml (original)
+++ commons/proper/configuration/branches/configuration2_experimental/xdocs/changes.xml Sat Mar 21 20:18:55 2009
@@ -85,6 +85,11 @@
     </release>
 
     <release version="1.7" date="in SVN" description="">
+      <action dev="oheger" type="add" issue="CONFIGURATION-370">
+        PropertiesConfiguration now defines a nested interface IOFactory. Using
+        this interface it is possible to inject custom PropertiesReader and
+        PropertiesWriter implementations.
+      </action>
       <action dev="oheger" type="fix" issue="CONFIGURATION-368">
         SubnodeConfiguration now fires an event of type EVENT_SUBNODE_CHANGED
         if a structural change of the parent configuration was detected. If the

Modified: commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/howto_properties.xml
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/howto_properties.xml?rev=757004&r1=757003&r2=757004&view=diff
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/howto_properties.xml (original)
+++ commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/howto_properties.xml Sat Mar 21 20:18:55 2009
@@ -207,6 +207,124 @@
         the <code>setForceSingleLine()</code> method can be used.
       </p>
       </subsection>
+
+      <subsection name="Custom properties readers and writers">
+      <p>
+        There are situations when more control over the process of reading and
+        writing properties file is needed. For instance, an application might
+        have to deal with some legacy properties file in a specific format,
+        which is not supported out of the box by
+        <code>PropertiesConfiguration</code>, but must not be modified. In these
+        cases it is possible to inject a custom reader and writer for
+        properties files.
+      </p>
+      <p>
+        Per default properties files are read and written by the nested classes
+        <code>PropertiesReader</code> and <code>PropertiesWriter</code>
+        (defined within <code>PropertiesConfiguration</code>). These classes are
+        regular reader and writer classes (both are derived from typical base
+        classes of the <code>java.io</code> package) that provide some
+        additional methods making dealing with properties files more
+        convenient. Custom implementations of properties readers and writers
+        must extend these base classes.
+      </p>
+      <p>
+        For installing a custom properties reader or writer
+        <code>PropertiesConfiguration</code> provides the <code>IOFactory</code>
+        interface (which is also defined as a nested class). An object
+        implementing this interface is stored in each
+        <code>PropertiesConfiguration</code> instance. Whenever a properties
+        file is to be read or written (i.e. when one of the <code>load()</code>
+        or <code>save()</code> methods is called), the <code>IOFactory</code>
+        object is asked for creating the properties reader or writer to be
+        used.
+      </p>
+      <p>
+        The <code>IOFactory</code> interface is pretty simple; it defines one
+        method for creating a properties reader and another one for creating a
+        properties writer. A default implementation called
+        <code>DefaultIOFactory</code> is also available and is used by
+        <code>PropertiesConfiguration</code> when no specific
+        <code>IOFactory</code> is set. To make this discussion more concrete
+        we provide an example of how to inject a custom properties reader. The
+        use case is that we have to load a properties file that contains keys
+        with whitespace, which is not supported by
+        <code>PropertiesConfiguration</code> per default. A fragment from such
+        a properties file could look as follows:
+      </p>
+        <source><![CDATA[
+Background Color = #800080
+Foreground Color = #000080
+]]></source>
+      <p>
+        The first step is to create a custom properties reader implementation
+        that can deal with such properties. The class is derived from
+        <code>PropertiesConfiguration.PropertiesReader</code> and overrides the
+        <code>parseProperty()</code> method:
+      </p>
+        <source><![CDATA[
+public class WhitespacePropertiesReader extends PropertiesConfiguration.PropertiesReader
+{
+    public WhitespacePropertiesReader(Reader in, char delimiter)
+    {
+        super(in, delimiter);
+    }
+    
+    /**
+     * Special algorithm for parsing properties keys with whitespace. This
+     * method is called for each non-comment line read from the properties
+     * file.
+     */
+    protected void parseProperty(String line)
+    {
+        // simply split the line at the first '=' character
+        // (this should be more robust in production code)
+        int pos = line.indexOf('=');
+        String key = line.substring(0, pos).trim();
+        String value = line.substring(pos + 1).trim();
+
+        // now store the key and the value of the property
+        initPropertyName(key);
+        initPropertyValue(value);
+    }
+}
+]]></source>
+      <p>
+        Notice the calls to the methods <code>initPropertyName()</code> and
+        <code>initPropertyValue()</code>. Here the results of the parsing
+        operation are stored. The next step is to provide a specialized
+        implementation of the <code>IOFactory</code> interface that returns
+        the new properties reader class. As we only want to replace the
+        properties reader (and use the standard writer), we can derive our
+        implementation from <code>DefaultIOFactory</code> and thus only have
+        to override the <code>createPropertiesReader()</code> method.
+      </p>
+        <source><![CDATA[
+public class WhitespaceIOFactory extends PropertiesConfiguration.DefaultIOFactory
+{
+    /**
+     * Return our special properties reader.
+     */
+    public PropertiesReader createPropertiesReader(Reader in, char delimiter)
+    {
+        return new WhitespacePropertiesReader(in, delimiter);
+    }
+}
+]]></source>
+      <p>
+        Finally an instance of our new <code>IOFactory</code> implementation
+        has to be created and passed to the <code>PropertiesConfiguration</code>
+        object. This must be done before the <code>load()</code> method is
+        called. So we cannot use one of the constructors that load the data.
+        The code for setting up the configuration object could look as follows:
+      </p>
+        <source><![CDATA[
+PropertiesConfiguration config = new PropertiesConfiguration();
+config.setIOFactory(new WhitespaceIOFactory());
+config.setFile(...);  // set the file to be loaded
+config.load();
+]]></source>
+      </subsection>
     </section>
 
   </body>

Modified: commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/user_guide.xml
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/user_guide.xml?rev=757004&r1=757003&r2=757004&view=diff
==============================================================================
--- commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/user_guide.xml (original)
+++ commons/proper/configuration/branches/configuration2_experimental/xdocs/userguide/user_guide.xml Sat Mar 21 20:18:55 2009
@@ -67,6 +67,7 @@
         <li><a href="howto_properties.html#Saving">Saving</a></li>
         <li><a href="howto_properties.html#Special_Characters">Special Characters</a></li>
         <li><a href="howto_properties.html#Layout_Objects">Layout Objects</a></li>
+        <li><a href="howto_properties.html#Custom_properties_readers_and_writers">Custom properties readers and writers</a></li>
       </ul>
       <li><a href="howto_filebased.html#File-based_Configurations">File-based Configurations</a></li>
       <ul>