Author: oheger Date: Sat Apr 21 07:31:58 2007 New Revision: 531038 URL: http://svn.apache.org/viewvc?view=rev&rev=531038 Log: CONFIGURATION-264: Added a new mode to SubnodeConfiguration, in which it checks for structural changes of its parent. In this mode it is able to detect reloads, too.
Modified: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/HierarchicalConfiguration.java jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/SubnodeConfiguration.java jakarta/commons/proper/configuration/trunk/src/test/org/apache/commons/configuration/TestSubnodeConfiguration.java jakarta/commons/proper/configuration/trunk/xdocs/changes.xml jakarta/commons/proper/configuration/trunk/xdocs/userguide/howto_xml.xml Modified: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/HierarchicalConfiguration.java URL: http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/HierarchicalConfiguration.java?view=diff&rev=531038&r1=531037&r2=531038 ============================================================================== --- jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/HierarchicalConfiguration.java (original) +++ jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/HierarchicalConfiguration.java Sat Apr 21 07:31:58 2007 @@ -458,13 +458,36 @@ * <code>SubnodeConfiguration</code> class to obtain further information * about subnode configurations and when they should be used. * </p> + * <p> + * With the <code>supportUpdate</code> flag the behavior of the returned + * <code>SubnodeConfiguration</code> regarding updates of its parent + * configuration can be determined. A subnode configuration operates on the + * same nodes as its parent, so changes at one configuration are normally + * directly visible for the other configuration. There are however changes + * of the parent configuration, which are not recognized by the subnode + * configuration per default. An example for this is a reload operation (for + * file-based configurations): Here the complete node set of the parent + * configuration is replaced, but the subnode configuration still references + * the old nodes. If such changes should be detected by the subnode + * configuration, the <code>supportUpdates</code> flag must be set to + * <b>true</b>. This causes the subnode configuration to reevaluate the key + * used for its creation each time it is accessed. This guarantees that the + * subnode configuration always stays in sync with its key, even if the + * parent configuration's data significantly changes. If such a change + * makes the key invalid - because it now no longer points to exactly one + * node -, the subnode configuration is not reconstructed, but keeps its + * old data. It is then quasi detached from its parent. + * </p> * * @param key the key that selects the sub tree + * @param supportUpdates a flag whether the returned subnode configuration + * should be able to handle updates of its parent * @return a hierarchical configuration that contains this sub tree * @see SubnodeConfiguration - * @since 1.3 + * @since 1.5 */ - public SubnodeConfiguration configurationAt(String key) + public SubnodeConfiguration configurationAt(String key, + boolean supportUpdates) { List nodes = fetchNodeList(key); if (nodes.size() != 1) @@ -472,7 +495,24 @@ throw new IllegalArgumentException( "Passed in key must select exactly one node: " + key); } - return createSubnodeConfiguration((ConfigurationNode) nodes.get(0)); + return supportUpdates ? createSubnodeConfiguration( + (ConfigurationNode) nodes.get(0), key) + : createSubnodeConfiguration((ConfigurationNode) nodes.get(0)); + } + + /** + * Returns a hierarchical subnode configuration for the node specified by + * the given key. This is a short form for <code>configurationAt(key, + * <b>false</b>)</code>. + * + * @param key the key that selects the sub tree + * @return a hierarchical configuration that contains this sub tree + * @see SubnodeConfiguration + * @since 1.3 + */ + public SubnodeConfiguration configurationAt(String key) + { + return configurationAt(key, false); } /** @@ -525,6 +565,24 @@ protected SubnodeConfiguration createSubnodeConfiguration(ConfigurationNode node) { return new SubnodeConfiguration(this, node); + } + + /** + * Creates a new subnode configuration for the specified node and sets its + * construction key. A subnode configuration created this way will be aware + * of structural changes of its parent. + * + * @param node the node, for which a subnode configuration is to be created + * @param subnodeKey the key used to construct the configuration + * @return the configuration for the given node + * @since 1.5 + */ + protected SubnodeConfiguration createSubnodeConfiguration( + ConfigurationNode node, String subnodeKey) + { + SubnodeConfiguration result = createSubnodeConfiguration(node); + result.setSubnodeKey(subnodeKey); + return result; } /** Modified: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/SubnodeConfiguration.java URL: http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/SubnodeConfiguration.java?view=diff&rev=531038&r1=531037&r2=531038 ============================================================================== --- jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/SubnodeConfiguration.java (original) +++ jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/SubnodeConfiguration.java Sat Apr 21 07:31:58 2007 @@ -16,6 +16,11 @@ */ package org.apache.commons.configuration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + import org.apache.commons.configuration.tree.ConfigurationNode; /** @@ -43,6 +48,48 @@ * configuration's root node is involved. * </p> * <p> + * There are however changes at the parent configuration, which cause the + * subnode configuration to become detached. An example for such a change is a + * reload operation of a file-based configuration, which replaces all nodes of + * the parent configuration. The subnode configuration per default still + * references the old nodes. Another example are list structures: a subnode + * configuration can be created to point on the <em>i</em>th element of the + * list. Now list elements can be added or removed, so that the list elements' + * indices change. In such a scenario the subnode configuration would always + * point to the same list element, regardless of its current index. + * </p> + * <p> + * To solve these problems and make a subnode configuration aware of + * such structural changes of its parent, it is possible to associate a + * subnode configuration with a configuration key. This can be done by calling + * the <code>setSubnodeKey()</code> method. If here a key is set, the subnode + * configuration will evaluate it on each access, thus ensuring that it is + * always in sync with its parent. In this mode the subnode configuration really + * behaves like a live-view on its parent. The price for this is a decreased + * performance because now an additional evaluation has to be performed on each + * property access. So this mode should only be used if necessary; if for + * instance a subnode configuration is only used for a temporary convenient + * access to a complex configuration, there is no need to make it aware for + * structural changes of its parent. If a subnode configuration is created + * using the <code>[EMAIL PROTECTED] HierarchicalConfiguration#configurationAt(String, boolean) + * configurationAt()}</code> method of <code>HierarchicalConfiguration</code> + * (which should be the preferred way), with an additional boolean parameter it + * can be specified whether the resulting subnode configuration should be + * aware of structural changes or not. Then the configuration key will be + * automatically set. + * </p> + * <p> + * <em>Note:</em> At the moment support for creating a subnode configuration + * that is aware of structural changes of its parent from another subnode + * configuration (a "sub subnode configuration") is limited. This only works if + * <ol><li>the subnode configuration that serves as the parent for the new + * subnode configuration is itself associated with a configuration key and</li> + * <li>the key passed in to create the new subnode configuration is not too + * complex (if configuration keys are used that contain indices, a corresponding + * key that is valid from the parent configuration's point of view cannot be + * constructed).</li></ol> + * </p> + * <p> * When a subnode configuration is created, it inherits the settings of its * parent configuration, e.g. some flags like the * <code>throwExceptionOnMissing</code> flag or the settings for handling list @@ -76,6 +123,9 @@ /** Stores the parent configuration. */ private HierarchicalConfiguration parent; + /** Stores the key that was used to construct this configuration.*/ + private String subnodeKey; + /** * Creates a new instance of <code>SubnodeConfiguration</code> and * initializes it with the parent configuration and the new root node. @@ -111,6 +161,80 @@ } /** + * Returns the key that was used to construct this configuration. If here a + * non-<b>null</b> value is returned, the subnode configuration will + * always check its parent for structural changes and reconstruct itself if + * necessary. + * + * @return the key for selecting this configuration's root node + * @since 1.5 + */ + public String getSubnodeKey() + { + return subnodeKey; + } + + /** + * Sets the key to the root node of this subnode configuration. If here a + * key is set, the subnode configuration will behave like a live-view on its + * parent for this key. See the class comment for more details. + * + * @param subnodeKey the key used to construct this configuration + * @since 1.5 + */ + public void setSubnodeKey(String subnodeKey) + { + this.subnodeKey = subnodeKey; + } + + /** + * Returns the root node for this configuration. If a subnode key is set, + * this implementation re-evaluates this key to find out if this subnode + * configuration needs to be reconstructed. This ensures that the subnode + * configuration is always synchronized with its parent configuration. + * + * @return the root node of this configuration + * @since 1.5 + * @see #setSubnodeKey(String) + */ + public ConfigurationNode getRootNode() + { + if (getSubnodeKey() != null) + { + try + { + List nodes = getParent().fetchNodeList(getSubnodeKey()); + if (nodes.size() != 1) + { + // key is invalid, so detach this subnode configuration + setSubnodeKey(null); + } + else + { + ConfigurationNode currentRoot = (ConfigurationNode) nodes + .get(0); + if (currentRoot != super.getRootNode()) + { + // the root node was changed due to a change of the + // parent + setRootNode(currentRoot); + } + return currentRoot; + } + } + catch (Exception ex) + { + // Evaluation of the key caused an exception. Probably the + // expression engine has changed on the parent. Detach this + // configuration, there is not much we can do about this. + setSubnodeKey(null); + } + } + + return super.getRootNode(); // use stored root node + } + + /** * Returns a hierarchical configuration object for the given sub node. * This implementation will ensure that the returned * <code>SubnodeConfiguration</code> object will have the same parent than @@ -122,6 +246,53 @@ protected SubnodeConfiguration createSubnodeConfiguration(ConfigurationNode node) { return new SubnodeConfiguration(getParent(), node); + } + + /** + * Returns a hierarchical configuration object for the given sub node that + * is aware of structural changes of its parent. Works like the method with + * the same name, but also sets the subnode key for the new subnode + * configuration, so it can check whether the parent has been changed. This + * only works if this subnode configuration has itself a valid subnode key. + * So if a subnode configuration that should be aware of structural changes + * is created from an already existing subnode configuration, this subnode + * configuration must also be aware of such changes. + * + * @param node the sub node, for which the configuration is to be created + * @param subnodeKey the construction key + * @return a hierarchical configuration for this sub node + * @since 1.5 + */ + protected SubnodeConfiguration createSubnodeConfiguration( + ConfigurationNode node, String subnodeKey) + { + SubnodeConfiguration result = createSubnodeConfiguration(node); + + if (getSubnodeKey() != null) + { + // construct the correct subnode key + // determine path to root node + List lstPathToRoot = new ArrayList(); + ConfigurationNode top = super.getRootNode(); + ConfigurationNode nd = node; + while (nd != top) + { + lstPathToRoot.add(nd); + nd = nd.getParentNode(); + } + + // construct the keys for the nodes on this path + Collections.reverse(lstPathToRoot); + String key = getSubnodeKey(); + for (Iterator it = lstPathToRoot.iterator(); it.hasNext();) + { + key = getParent().getExpressionEngine().nodeKey( + (ConfigurationNode) it.next(), key); + } + result.setSubnodeKey(key); + } + + return result; } /** Modified: jakarta/commons/proper/configuration/trunk/src/test/org/apache/commons/configuration/TestSubnodeConfiguration.java URL: http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/src/test/org/apache/commons/configuration/TestSubnodeConfiguration.java?view=diff&rev=531038&r1=531037&r2=531038 ============================================================================== --- jakarta/commons/proper/configuration/trunk/src/test/org/apache/commons/configuration/TestSubnodeConfiguration.java (original) +++ jakarta/commons/proper/configuration/trunk/src/test/org/apache/commons/configuration/TestSubnodeConfiguration.java Sat Apr 21 07:31:58 2007 @@ -16,12 +16,14 @@ */ package org.apache.commons.configuration; +import java.io.File; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.configuration.reloading.FileAlwaysReloadingStrategy; import org.apache.commons.configuration.tree.ConfigurationNode; import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine; @@ -45,6 +47,12 @@ { "docid", "docname", "author", "dateOfCreation", "version", "size" }, { "userid", "uname", "firstName", "lastName" } }; + /** Constant for a test output file.*/ + private static final File TEST_FILE = new File("target/test.xml"); + + /** Constant for an updated table name.*/ + private static final String NEW_TABLE_NAME = "newTable"; + /** The parent configuration. */ HierarchicalConfiguration parent; @@ -64,6 +72,15 @@ nodeCounter = 0; } + protected void tearDown() throws Exception + { + // remove the test output file if necessary + if (TEST_FILE.exists()) + { + TEST_FILE.delete(); + } + } + /** * Tests creation of a subnode config. */ @@ -311,6 +328,134 @@ assertEquals("Wrong interpolation in subnode", "/home/foo/path" + i, sub.getString("dir" + i)); } + } + + /** + * Tests a reload operation for the parent configuration when the subnode + * configuration does not support reloads. Then the new value should not be + * detected. + */ + public void testParentReloadNotSupported() throws ConfigurationException + { + Configuration c = setUpReloadTest(false); + assertEquals("Name changed in sub config", TABLE_NAMES[1], config + .getString("name")); + assertEquals("Name not changed in parent", NEW_TABLE_NAME, c + .getString("tables.table(1).name")); + } + + /** + * Tests a reload operation for the parent configuration when the subnode + * configuration does support reloads. The new value should be returned. + */ + public void testParentReloadSupported() throws ConfigurationException + { + Configuration c = setUpReloadTest(true); + assertEquals("Name not changed in sub config", NEW_TABLE_NAME, config + .getString("name")); + assertEquals("Name not changed in parent", NEW_TABLE_NAME, c + .getString("tables.table(1).name")); + } + + /** + * Tests a reload operation for the parent configuration when the subnode + * configuration is aware of reloads, and the parent configuration is + * accessed first. The new value should be returned. + */ + public void testParentReloadSupportAccessParent() + throws ConfigurationException + { + Configuration c = setUpReloadTest(true); + assertEquals("Name not changed in parent", NEW_TABLE_NAME, c + .getString("tables.table(1).name")); + assertEquals("Name not changed in sub config", NEW_TABLE_NAME, config + .getString("name")); + } + + /** + * Tests whether reloads work with sub subnode configurations. + */ + public void testParentReloadSubSubnode() throws ConfigurationException + { + setUpReloadTest(true); + SubnodeConfiguration sub = config.configurationAt("fields", true); + assertEquals("Wrong subnode key", "tables.table(1).fields", sub + .getSubnodeKey()); + assertEquals("Changed field not detected", "newField", sub + .getString("field(0).name")); + } + + /** + * Tests creating a sub sub config when the sub config is not aware of + * changes. Then the sub sub config shouldn't be either. + */ + public void testParentReloadSubSubnodeNoChangeSupport() + throws ConfigurationException + { + setUpReloadTest(false); + SubnodeConfiguration sub = config.configurationAt("fields", true); + assertNull("Sub sub config is attached to parent", sub.getSubnodeKey()); + assertEquals("Changed field name returned", TABLE_FIELDS[1][0], sub + .getString("field(0).name")); + } + + /** + * Prepares a test for a reload operation. + * + * @param supportReload a flag whether the subnode configuration should + * support reload operations + * @return the parent configuration that can be used for testing + * @throws ConfigurationException if an error occurs + */ + private XMLConfiguration setUpReloadTest(boolean supportReload) + throws ConfigurationException + { + XMLConfiguration xmlConf = new XMLConfiguration(parent); + xmlConf.setFile(TEST_FILE); + xmlConf.save(); + config = xmlConf.configurationAt("tables.table(1)", supportReload); + assertEquals("Wrong table name", TABLE_NAMES[1], config + .getString("name")); + xmlConf.setReloadingStrategy(new FileAlwaysReloadingStrategy()); + // Now change the configuration file + XMLConfiguration confUpdate = new XMLConfiguration(TEST_FILE); + confUpdate.setProperty("tables.table(1).name", NEW_TABLE_NAME); + confUpdate.setProperty("tables.table(1).fields.field(0).name", + "newField"); + confUpdate.save(); + return xmlConf; + } + + /** + * Tests a manipulation of the parent configuration that causes the subnode + * configuration to become invalid. In this case the sub config should be + * detached and keep its old values. + */ + public void testParentChangeDetach() + { + final String key = "tables.table(1)"; + config = parent.configurationAt(key, true); + assertEquals("Wrong subnode key", key, config.getSubnodeKey()); + assertEquals("Wrong table name", TABLE_NAMES[1], config + .getString("name")); + parent.clearTree(key); + assertEquals("Wrong table name after change", TABLE_NAMES[1], config + .getString("name")); + assertNull("Sub config was not detached", config.getSubnodeKey()); + } + + /** + * Tests detaching a subnode configuration when an exception is thrown + * during reconstruction. This can happen e.g. if the expression engine is + * changed for the parent. + */ + public void testParentChangeDetatchException() + { + config = parent.configurationAt("tables.table(1)", true); + parent.setExpressionEngine(new XPathExpressionEngine()); + assertEquals("Wrong name of table", TABLE_NAMES[1], config + .getString("name")); + assertNull("Sub config was not detached", config.getSubnodeKey()); } /** Modified: jakarta/commons/proper/configuration/trunk/xdocs/changes.xml URL: http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/xdocs/changes.xml?view=diff&rev=531038&r1=531037&r2=531038 ============================================================================== --- jakarta/commons/proper/configuration/trunk/xdocs/changes.xml (original) +++ jakarta/commons/proper/configuration/trunk/xdocs/changes.xml Sat Apr 21 07:31:58 2007 @@ -23,6 +23,13 @@ <body> <release version="1.5-SNAPSHOT" date="in SVN" description=""> + <action dev="oheger" type="add" issue="CONFIGURATION-264"> + A SubnodeConfiguration per default does not see certain changes of its + parent configuration (e.g. reloads). With a new boolean parameter of + HierarchicalConfiguration's configurationAt() method a mode can be + enabled, in which the subnode configuration checks for such changes and + reconstructs itself if necessary. + </action> <action dev="ebourg" type="fix"> byte[] properties are properly saved as data fields in the plist configurations (PropertyListConfiguration and XMLPropertyListConfiguration). Modified: jakarta/commons/proper/configuration/trunk/xdocs/userguide/howto_xml.xml URL: http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/xdocs/userguide/howto_xml.xml?view=diff&rev=531038&r1=531037&r2=531038 ============================================================================== --- jakarta/commons/proper/configuration/trunk/xdocs/userguide/howto_xml.xml (original) +++ jakarta/commons/proper/configuration/trunk/xdocs/userguide/howto_xml.xml Sat Apr 21 07:31:58 2007 @@ -393,6 +393,14 @@ String fieldType = sub.getString("type"); ... ]]></source> + <p> + The configurations returned by the <code>configurationAt()</code> and + <code>configurationsAt()</code> method are in fact instances of the + <a href="apidocs/org/apache/commons/configuration/SubnodeConfiguration.html"> + <code>SubnodeConfiguration</code></a> class. The API documentation of + this class contains more information about its features and + limitations. + </p> </subsection> <subsection name="Adding new properties"> <p> --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]