You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@commons.apache.org by oh...@apache.org on 2007/04/21 16:31:59 UTC

svn commit: r531038 - in /jakarta/commons/proper/configuration/trunk: src/java/org/apache/commons/configuration/ src/test/org/apache/commons/configuration/ xdocs/ xdocs/userguide/

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>{@link 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: commons-dev-unsubscribe@jakarta.apache.org
For additional commands, e-mail: commons-dev-help@jakarta.apache.org