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>