Author: oheger
Date: Fri May 10 19:16:40 2013
New Revision: 1481154

URL: http://svn.apache.org/r1481154
Log:
Reworked SubnodeConfiguration to use a Synchronizer.

A SubnodeConfiguration per default shares its Synchronizer with its parent
because both configurations operate on the same node set. In the past
SubnodeConfiguration instances created with a subnode key (for detecting
structural changes of its parent) checked their root nodes on each read
access. Now such checks are performed when the parent configuration is
changed.

Modified:
    
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/BaseHierarchicalConfiguration.java
    
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/INIConfiguration.java
    
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java
    
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestBaseHierarchicalConfigurationSynchronization.java
    
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java
    
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java

Modified: 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/BaseHierarchicalConfiguration.java
URL: 
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/BaseHierarchicalConfiguration.java?rev=1481154&r1=1481153&r2=1481154&view=diff
==============================================================================
--- 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/BaseHierarchicalConfiguration.java
 (original)
+++ 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/BaseHierarchicalConfiguration.java
 Fri May 10 19:16:40 2013
@@ -21,12 +21,15 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.Stack;
+import java.util.WeakHashMap;
 
 import org.apache.commons.configuration.event.ConfigurationEvent;
 import org.apache.commons.configuration.event.ConfigurationListener;
@@ -177,11 +180,20 @@ public class BaseHierarchicalConfigurati
     private transient ExpressionEngine expressionEngine;
 
     /**
+     * A map for managing the {@code SubnodeConfiguration} instances created
+     * from this configuration.
+     */
+    private Map<SubnodeConfiguration, Object> subConfigs;
+
+    /** A listener for reacting on changes to update sub configurations. */
+    private ConfigurationListener changeListener;
+
+    /**
      * Creates a new instance of {@code BaseHierarchicalConfiguration}.
      */
     public BaseHierarchicalConfiguration()
     {
-        rootNode = new DefaultConfigurationNode();
+        this(new DefaultConfigurationNode());
     }
 
     /**
@@ -195,11 +207,19 @@ public class BaseHierarchicalConfigurati
      */
     public BaseHierarchicalConfiguration(HierarchicalConfiguration c)
     {
-        this();
-        if (c != null)
-        {
-            rootNode = copyRootNode(c);
-        }
+        this(copyRootNode(c));
+    }
+
+    /**
+     * Creates a new instance of {@code BaseHierarchicalConfiguration} with the
+     * passed in node as root node.
+     *
+     * @param root the root node (not <b>null</b>)
+     * @since 2.0
+     */
+    protected BaseHierarchicalConfiguration(ConfigurationNode root)
+    {
+        rootNode = root;
     }
 
     /**
@@ -627,15 +647,22 @@ public class BaseHierarchicalConfigurati
     public SubnodeConfiguration configurationAt(String key,
             boolean supportUpdates)
     {
-        List<ConfigurationNode> nodes = fetchNodeList(key);
-        if (nodes.size() != 1)
+        beginWrite();
+        try
         {
-            throw new IllegalArgumentException(
-                    "Passed in key must select exactly one node: " + key);
+            List<ConfigurationNode> nodes = fetchNodeList(key);
+            if (nodes.size() != 1)
+            {
+                throw new IllegalArgumentException(
+                        "Passed in key must select exactly one node: " + key);
+            }
+            return createAndInitializeSubnodeConfiguration(nodes.get(0), key,
+                    supportUpdates);
+        }
+        finally
+        {
+            endWrite();
         }
-        return supportUpdates ? createSubnodeConfiguration(
-                nodes.get(0), key)
-                : createSubnodeConfiguration(nodes.get(0));
     }
 
     /**
@@ -706,13 +733,23 @@ public class BaseHierarchicalConfigurati
      */
     public List<SubnodeConfiguration> configurationsAt(String key)
     {
-        List<ConfigurationNode> nodes = fetchNodeList(key);
-        List<SubnodeConfiguration> configs = new 
ArrayList<SubnodeConfiguration>(nodes.size());
-        for (ConfigurationNode node : nodes)
+        beginWrite();
+        try
+        {
+            List<ConfigurationNode> nodes = fetchNodeList(key);
+            List<SubnodeConfiguration> configs =
+                    new ArrayList<SubnodeConfiguration>(nodes.size());
+            for (ConfigurationNode node : nodes)
+            {
+                configs.add(createAndInitializeSubnodeConfiguration(node, null,
+                        false));
+            }
+            return configs;
+        }
+        finally
         {
-            configs.add(createSubnodeConfiguration(node));
+            endWrite();
         }
-        return configs;
     }
 
     /**
@@ -734,20 +771,29 @@ public class BaseHierarchicalConfigurati
      */
     public List<SubnodeConfiguration> childConfigurationsAt(String key)
     {
-        List<ConfigurationNode> nodes = fetchNodeList(key);
-        if (nodes.size() != 1)
+        beginWrite();
+        try
         {
-            return Collections.emptyList();
-        }
+            List<ConfigurationNode> nodes = fetchNodeList(key);
+            if (nodes.size() != 1)
+            {
+                return Collections.emptyList();
+            }
 
-        ConfigurationNode parent = nodes.get(0);
-        List<SubnodeConfiguration> subs =
-                new ArrayList<SubnodeConfiguration>(parent.getChildrenCount());
-        for (ConfigurationNode c : parent.getChildren())
+            ConfigurationNode parent = nodes.get(0);
+            List<SubnodeConfiguration> subs =
+                    new ArrayList<SubnodeConfiguration>(
+                            parent.getChildrenCount());
+            for (ConfigurationNode c : parent.getChildren())
+            {
+                subs.add(createAndInitializeSubnodeConfiguration(c, null, 
false));
+            }
+            return subs;
+        }
+        finally
         {
-            subs.add(createSubnodeConfiguration(c));
+            endWrite();
         }
-        return subs;
     }
 
     /**
@@ -763,27 +809,13 @@ public class BaseHierarchicalConfigurati
     }
 
     /**
-     * Creates a subnode configuration for the specified node. This method is
-     * called by {@code configurationAt()} and
-     * {@code configurationsAt()}.
-     *
-     * @param node the node, for which a subnode configuration is to be created
-     * @return the configuration for the given node
-     * @since 1.3
-     */
-    protected SubnodeConfiguration 
createSubnodeConfiguration(ConfigurationNode node)
-    {
-        SubnodeConfiguration result = new SubnodeConfiguration(this, node);
-        registerSubnodeConfiguration(result);
-        return result;
-    }
-
-    /**
-     * 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.
+     * Creates a new {@code SubnodeConfiguration} for the specified node and
+     * sets its construction key. If the key is not <b>null</b>, a
+     * {@code SubnodeConfiguration} 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 node the node, for which a {@code SubnodeConfiguration} is to be
+     *        created
      * @param subnodeKey the key used to construct the configuration
      * @return the configuration for the given node
      * @since 1.5
@@ -791,9 +823,7 @@ public class BaseHierarchicalConfigurati
     protected SubnodeConfiguration createSubnodeConfiguration(
             ConfigurationNode node, String subnodeKey)
     {
-        SubnodeConfiguration result = createSubnodeConfiguration(node);
-        result.setSubnodeKey(subnodeKey);
-        return result;
+        return new SubnodeConfiguration(this, node, subnodeKey);
     }
 
     /**
@@ -811,22 +841,117 @@ public class BaseHierarchicalConfigurati
     }
 
     /**
-     * Registers this instance at the given subnode configuration. This
-     * implementation will register a change listener, so that modifications of
-     * the subnode configuration can be tracked.
+     * Creates a new {@code SubnodeConfiguration} instance from this
+     * configuration and initializes it. This method also takes care that data
+     * structures are created to manage all {@code SubnodeConfiguration}
+     * instances with support for updates. They are stored, so that they can be
+     * triggered when this configuration is changed.
+     *
+     * @param node the root node of the new {@code SubnodeConfiguration}
+     * @param key the key to this node
+     * @param supportUpdates a flag whether updates are supported
+     * @return the newly created and initialized {@code SubnodeConfiguration}
+     * @since 2.0
+     */
+    protected final SubnodeConfiguration 
createAndInitializeSubnodeConfiguration(
+            ConfigurationNode node, String key, boolean supportUpdates)
+    {
+        String subnodeKey = supportUpdates ? key : null;
+        SubnodeConfiguration sub = createSubnodeConfiguration(node, 
subnodeKey);
+
+        if (changeListener == null)
+        {
+            changeListener = createChangeListener();
+            subConfigs = new WeakHashMap<SubnodeConfiguration, Object>();
+            addConfigurationListener(changeListener);
+        }
+        sub.addConfigurationListener(changeListener);
+        sub.initSubConfigManagementData(subConfigs, changeListener);
+        sub.setSynchronizer(getSynchronizer());
+
+        if (supportUpdates)
+        {
+            // store this configuration so it can later be validated
+            subConfigs.put(sub, Boolean.TRUE);
+        }
+        return sub;
+    }
+
+    /**
+     * Initializes the data related to the management of
+     * {@code SubnodeConfiguration} instances. This method is called each time 
a
+     * new {@code SubnodeConfiguration} was created. A configuration and its
+     * {@code SubnodeConfiguration} instances operate on the same set of data.
      *
-     * @param config the subnode configuration
-     * @since 1.5
+     * @param subMap the map with all {@code SubnodeConfiguration} instances
+     * @param listener the listener for reacting on changes
+     */
+    void initSubConfigManagementData(Map<SubnodeConfiguration, Object> subMap,
+            ConfigurationListener listener)
+    {
+        subConfigs = subMap;
+        changeListener = listener;
+    }
+
+    /**
+     * Creates a listener which reacts on all changes on this configuration or
+     * one of its {@code SubnodeConfiguration} instances. If such a change is
+     * detected, some updates have to be performed.
+     *
+     * @return the newly created change listener
      */
-    void registerSubnodeConfiguration(SubnodeConfiguration config)
+    private ConfigurationListener createChangeListener()
     {
-        config.addConfigurationListener(new ConfigurationListener()
+        return new ConfigurationListener()
         {
             public void configurationChanged(ConfigurationEvent event)
             {
-                subnodeConfigurationChanged(event);
+                nodeStructureChanged(event);
             }
-        });
+        };
+    }
+
+    /**
+     * A change on the node structure of this configuration has been detected.
+     * This can be caused either by an update of this configuration or by one 
if
+     * its {@code SubnodeConfiguration} instances. This method calls
+     * {@link #subnodeConfigurationChanged(ConfigurationEvent)} if necessary 
and
+     * ensures that all {@code SubnodeConfiguration} instances are validated.
+     * Note: when this method is called, a write lock is held on this
+     * configuration.
+     *
+     * @param event the change event
+     */
+    private void nodeStructureChanged(ConfigurationEvent event)
+    {
+        if (this != event.getSource())
+        {
+            subnodeConfigurationChanged(event);
+        }
+
+        if (!event.isBeforeUpdate() && EVENT_SUBNODE_CHANGED != 
event.getType())
+        {
+            validSubnodeConfigurations(event);
+        }
+    }
+
+    /**
+     * Triggers validation on all {@code SubnodeConfiguration} instances 
created
+     * by this configuration.
+     *
+     * @param event the change event
+     */
+    private void validSubnodeConfigurations(ConfigurationEvent event)
+    {
+        Set<SubnodeConfiguration> subs =
+                new HashSet<SubnodeConfiguration>(subConfigs.keySet());
+        for (SubnodeConfiguration sub : subs)
+        {
+            if (sub != event.getSource())
+            {
+                sub.validateRootNode();
+            }
+        }
     }
 
     /**
@@ -1264,11 +1389,16 @@ public class BaseHierarchicalConfigurati
     /**
      * Creates a copy of the node structure of the passed in configuration.
      *
-     * @param c the configuration whose nodes are to be copied
+     * @param c the configuration whose nodes are to be copied (may be 
<b>null</b>)
      * @return the copied root node
      */
     private static ConfigurationNode copyRootNode(HierarchicalConfiguration c)
     {
+        if (c == null)
+        {
+            return new DefaultConfigurationNode();
+        }
+
         CloneVisitor visitor = new CloneVisitor();
         c.lock(LockMode.READ);
         try

Modified: 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/INIConfiguration.java
URL: 
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/INIConfiguration.java?rev=1481154&r1=1481153&r2=1481154&view=diff
==============================================================================
--- 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/INIConfiguration.java
 (original)
+++ 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/INIConfiguration.java
 Fri May 10 19:16:40 2013
@@ -274,7 +274,7 @@ public class INIConfiguration extends Ba
                 out.print(section);
                 out.print("]");
                 out.println();
-                subset = createSubnodeConfiguration(getSectionNode(section));
+                subset = createSubnodeConfiguration(getSectionNode(section), 
null);
             }
             else
             {
@@ -779,7 +779,7 @@ public class INIConfiguration extends Ba
             {
                 // the passed in key does not map to exactly one node
                 // obtain the node for the section, create it on demand
-                return new SubnodeConfiguration(this, getSectionNode(name));
+                return 
createAndInitializeSubnodeConfiguration(getSectionNode(name), null, false);
             }
         }
     }
@@ -827,7 +827,7 @@ public class INIConfiguration extends Ba
             }
         }
 
-        return createSubnodeConfiguration(parent);
+        return createAndInitializeSubnodeConfiguration(parent, null, false);
     }
 
     /**

Modified: 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java
URL: 
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java?rev=1481154&r1=1481153&r2=1481154&view=diff
==============================================================================
--- 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java
 (original)
+++ 
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/SubnodeConfiguration.java
 Fri May 10 19:16:40 2013
@@ -20,7 +20,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
-import org.apache.commons.configuration.reloading.Reloadable;
 import org.apache.commons.configuration.tree.ConfigurationNode;
 
 /**
@@ -50,9 +49,8 @@ import org.apache.commons.configuration.
  * <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
+ * {@code clearTree()} operation, which replaces the sub tree to which this
+ * configuration's root node belongs. 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
@@ -66,8 +64,8 @@ import org.apache.commons.configuration.
  * 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
+ * performance because now additional evaluation has to be performed to keep
+ * the root node up-to-date. 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
@@ -95,10 +93,19 @@ import org.apache.commons.configuration.
  * {@code throwExceptionOnMissing} flag or the settings for handling list
  * delimiters) or the expression engine. If these settings are changed later in
  * either the subnode or the parent configuration, the changes are not visible
- * for each other. So you could create a subnode configuration, change its
+ * for each other. So you could create a subnode configuration, and change its
  * expression engine without affecting the parent configuration.
  * </p>
  * <p>
+ * Because the {@code SubnodeConfiguration} operates on the same nodes
+ * structure as its parent it uses the same {@code Synchronizer} instance per
+ * default. This means that locks held on one {@code SubnodeConfiguration}
+ * also impact the parent configuration and all of its other {@code 
SubnodeConfiguration}
+ * objects. You should not change this without a good reason! Otherwise, there
+ * is the risk of data corruption when multiple threads access these
+ * configuration concurrently.
+ * </p>
+ * <p>
  * From its purpose this class is quite similar to
  * {@link SubsetConfiguration}. The difference is that a subset
  * configuration of a hierarchical configuration may combine multiple
@@ -108,6 +115,13 @@ import org.apache.commons.configuration.
  * class instead of {@code SubsetConfiguration} because creating a subset
  * configuration is more expensive than creating a subnode configuration.
  * </p>
+ * <p>
+ * It is strongly recommended to create {@code SubnodeConfiguration} instances
+ * only through the {@code configurationAt()} methods of a hierarchical
+ * configuration. These methods ensure that all necessary initializations are
+ * done. Creating instances manually without doing proper initialization may
+ * break some of the functionality provided by this class.
+ * </p>
  *
  * @since 1.3
  * @author <a
@@ -115,7 +129,7 @@ import org.apache.commons.configuration.
  * Configuration team</a>
  * @version $Id$
  */
-public class SubnodeConfiguration extends HierarchicalReloadableConfiguration
+public class SubnodeConfiguration extends BaseHierarchicalConfiguration
 {
     /**
      * The serial version UID.
@@ -129,15 +143,18 @@ public class SubnodeConfiguration extend
     private String subnodeKey;
 
     /**
-     * Creates a new instance of {@code SubnodeConfiguration} and
-     * initializes it with the parent configuration and the new root node.
+     * Creates a new instance of {@code SubnodeConfiguration} and initializes 
it
+     * with the parent configuration and the new root node.
      *
      * @param parent the parent configuration
-     * @param root the root node of this subnode configuration
+     * @param root the root node of this {@code SubnodeConfiguration}
+     * @param subKey the key associated with this {@code SubnodeConfiguration}
+     *        (can be <b>null</b>)
      */
-    public SubnodeConfiguration(BaseHierarchicalConfiguration parent, 
ConfigurationNode root)
+    public SubnodeConfiguration(BaseHierarchicalConfiguration parent,
+            ConfigurationNode root, String subKey)
     {
-        super(parent instanceof Reloadable ? ((Reloadable) 
parent).getReloadLock() : null);
+        super(root);
         if (parent == null)
         {
             throw new IllegalArgumentException(
@@ -148,8 +165,8 @@ public class SubnodeConfiguration extend
             throw new IllegalArgumentException("Root node must not be null!");
         }
 
-        setRootNode(root);
         this.parent = parent;
+        subnodeKey = subKey;
         initFromParent(parent);
         initInterpolator();
     }
@@ -175,7 +192,15 @@ public class SubnodeConfiguration extend
      */
     public String getSubnodeKey()
     {
-        return subnodeKey;
+        beginRead();
+        try
+        {
+            return subnodeKey;
+        }
+        finally
+        {
+            endRead();
+        }
     }
 
     /**
@@ -188,56 +213,15 @@ public class SubnodeConfiguration extend
      */
     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)
-     */
-    @Override
-    public ConfigurationNode getRootNode()
-    {
-        if (getSubnodeKey() != null)
+        beginWrite();
+        try
         {
-            try
-            {
-                List<ConfigurationNode> nodes = 
getParent().fetchNodeList(getSubnodeKey());
-                if (nodes.size() != 1)
-                {
-                    // key is invalid, so detach this subnode configuration
-                    setSubnodeKey(null);
-                }
-                else
-                {
-                    ConfigurationNode currentRoot = nodes.get(0);
-                    if (currentRoot != super.getRootNode())
-                    {
-                        // the root node was changed due to a change of the
-                        // parent
-                        fireEvent(EVENT_SUBNODE_CHANGED, null, null, true);
-                        setRootNode(currentRoot);
-                        fireEvent(EVENT_SUBNODE_CHANGED, null, null, false);
-                    }
-                    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);
-            }
+            this.subnodeKey = subnodeKey;
+        }
+        finally
+        {
+            endWrite();
         }
-
-        return super.getRootNode(); // use stored root node
     }
 
     /**
@@ -253,26 +237,17 @@ public class SubnodeConfiguration extend
      */
     public void clearAndDetachFromParent()
     {
-        clear();
-        setSubnodeKey(null); // always detach
-        getParent().removeNode(getRootNode());
-    }
-
-    /**
-     * Returns a hierarchical configuration object for the given sub node.
-     * This implementation will ensure that the returned
-     * {@code SubnodeConfiguration} object will have the same parent than
-     * this object.
-     *
-     * @param node the sub node, for which the configuration is to be created
-     * @return a hierarchical configuration for this sub node
-     */
-    @Override
-    protected SubnodeConfiguration 
createSubnodeConfiguration(ConfigurationNode node)
-    {
-        SubnodeConfiguration result = new SubnodeConfiguration(getParent(), 
node);
-        getParent().registerSubnodeConfiguration(result);
-        return result;
+        beginWrite();
+        try
+        {
+            clearInternal();
+            subnodeKey = null; // always detach
+            getParent().removeNode(getRootNode());
+        }
+        finally
+        {
+            endWrite();
+        }
     }
 
     /**
@@ -286,40 +261,18 @@ public class SubnodeConfiguration extend
      * 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
+     * @param subKey the construction key
      * @return a hierarchical configuration for this sub node
      * @since 1.5
      */
     @Override
     protected SubnodeConfiguration createSubnodeConfiguration(
-            ConfigurationNode node, String subnodeKey)
+            ConfigurationNode node, String subKey)
     {
-        SubnodeConfiguration result = createSubnodeConfiguration(node);
-
-        if (getSubnodeKey() != null)
-        {
-            // construct the correct subnode key
-            // determine path to root node
-            List<ConfigurationNode> lstPathToRoot = new 
ArrayList<ConfigurationNode>();
-            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 (ConfigurationNode pathNode : lstPathToRoot)
-            {
-                key = getParent().getExpressionEngine().nodeKey(pathNode, key);
-            }
-            result.setSubnodeKey(key);
-        }
-
-        return result;
+        String key =
+                (subKey != null && subnodeKey != null) ? 
constructSubKeyForSubnodeConfig(node)
+                        : null;
+        return new SubnodeConfiguration(getParent(), node, key);
     }
 
     /**
@@ -350,6 +303,47 @@ public class SubnodeConfiguration extend
     }
 
     /**
+     * Validates this configuration's root node. This method checks whether the
+     * key associated with this {@code SubnodeConfiguration} (if any) still
+     * points to a valid node in the parent configuration. If not, the key is
+     * cleared, and this configuration is now detached from its parent.
+     */
+    void validateRootNode()
+    {
+        if (subnodeKey != null)
+        {
+            try
+            {
+                List<ConfigurationNode> nodes = 
getParent().fetchNodeList(subnodeKey);
+                if (nodes.size() != 1)
+                {
+                    // key is invalid, so detach this subnode configuration
+                    subnodeKey = null;
+                }
+                else
+                {
+                    ConfigurationNode currentRoot = nodes.get(0);
+                    if (currentRoot != super.getRootNode())
+                    {
+                        // the root node was changed due to a change of the
+                        // parent
+                        fireEvent(EVENT_SUBNODE_CHANGED, null, null, true);
+                        setRootNode(currentRoot);
+                        fireEvent(EVENT_SUBNODE_CHANGED, null, null, false);
+                    }
+                }
+            }
+            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.
+                subnodeKey = null;
+            }
+        }
+    }
+
+    /**
      * Initializes the {@code ConfigurationInterpolator} for this sub 
configuration.
      * This is a standard {@code ConfigurationInterpolator} which also 
references
      * the {@code ConfigurationInterpolator} of the parent configuration.
@@ -358,4 +352,34 @@ public class SubnodeConfiguration extend
     {
         getInterpolator().setParentInterpolator(getParent().getInterpolator());
     }
+
+    /**
+     * Constructs the key for a {@code SubnodeConfiguration} for associating it
+     * with a node in the parent configuration. This method creates a canonical
+     * key based on the path from the given node to the root node.
+     *
+     * @param node the root node for the new {@code SubnodeConfiguration}
+     * @return the key for this {@code SubnodeConfiguration}
+     */
+    private String constructSubKeyForSubnodeConfig(ConfigurationNode node)
+    {
+        List<ConfigurationNode> lstPathToRoot =
+                new ArrayList<ConfigurationNode>();
+        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 = subnodeKey;
+        for (ConfigurationNode pathNode : lstPathToRoot)
+        {
+            key = getParent().getExpressionEngine().nodeKey(pathNode, key);
+        }
+        return key;
+    }
 }

Modified: 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestBaseHierarchicalConfigurationSynchronization.java
URL: 
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestBaseHierarchicalConfigurationSynchronization.java?rev=1481154&r1=1481153&r2=1481154&view=diff
==============================================================================
--- 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestBaseHierarchicalConfigurationSynchronization.java
 (original)
+++ 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestBaseHierarchicalConfigurationSynchronization.java
 Fri May 10 19:16:40 2013
@@ -17,10 +17,14 @@
 package org.apache.commons.configuration;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.util.Collections;
+import java.util.List;
 
 import org.apache.commons.configuration.SynchronizerTestImpl.Methods;
 import org.apache.commons.configuration.io.FileHandler;
@@ -130,4 +134,121 @@ public class TestBaseHierarchicalConfigu
         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
         assertNotSame("Synchronizer was copied", sync, copy.getSynchronizer());
     }
+
+    /**
+     * Tests whether synchronization is performed when constructing a
+     * SubnodeConfiguration.
+     */
+    @Test
+    public void testConfigurationAtSynchronized()
+    {
+        SubnodeConfiguration sub = config.configurationAt("element2");
+        assertEquals("Wrong property", "I'm complex!",
+                sub.getString("subelement.subsubelement"));
+        sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_READ,
+                Methods.END_READ);
+    }
+
+    /**
+     * Tests whether synchronization is performed when constructing multiple
+     * SubnodeConfiguration objects.
+     */
+    @Test
+    public void testConfigurationsAtSynchronized()
+    {
+        List<SubnodeConfiguration> subs = config.configurationsAt("list.item");
+        assertFalse("No subnode configurations", subs.isEmpty());
+        sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
+    }
+
+    /**
+     * Tests whether childConfigurationsAt() is correctly synchronized.
+     */
+    @Test
+    public void testChildConfigurationsAtSynchronized()
+    {
+        List<SubnodeConfiguration> subs = 
config.childConfigurationsAt("clear");
+        assertFalse("No subnode configurations", subs.isEmpty());
+        sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE);
+    }
+
+    /**
+     * Tests whether synchronization is performed when setting the key of a
+     * SubnodeConfiguration.
+     */
+    @Test
+    public void testSetSubnodeKeySynchronized()
+    {
+        SubnodeConfiguration sub = config.configurationAt("element2");
+        assertNull("Got a subnode key", sub.getSubnodeKey());
+        sub.setSubnodeKey("element2");
+        // 1 x configurationAt(), 1 x getSubnodeKey(), 1 x setSubnodeKey()
+        sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_READ,
+                Methods.END_READ, Methods.BEGIN_WRITE, Methods.END_WRITE);
+    }
+
+    /**
+     * Tests whether synchronization is performed when querying the key of a
+     * SubnodeConfiguration.
+     */
+    @Test
+    public void testGetSubnodeKeySynchronized()
+    {
+        SubnodeConfiguration sub = config.configurationAt("element2", true);
+        assertEquals("Wrong subnode key", "element2", sub.getSubnodeKey());
+        // 1 x configurationAt(), 1 x getSubnodeKey()
+        sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE,
+                Methods.BEGIN_READ, Methods.END_READ);
+    }
+
+    /**
+     * Tests whether updates on nodes are communicated to all
+     * SubnodeConfigurations of a configuration.
+     */
+    @Test
+    public void testSubnodeUpdate()
+    {
+        config.addProperty("element2.test", Boolean.TRUE);
+        SubnodeConfiguration sub = config.configurationAt("element2", true);
+        SubnodeConfiguration subsub = sub.configurationAt("subelement", true);
+        config.clearTree("element2.subelement");
+        assertNotNull("Sub1 detached", sub.getSubnodeKey());
+        assertNull("Sub2 still attached", subsub.getSubnodeKey());
+    }
+
+    /**
+     * Tests whether updates caused by a SubnodeConfiguration are communicated
+     * to all other SubnodeConfigurations.
+     */
+    @Test
+    public void testSubnodeUpdateBySubnode()
+    {
+        SubnodeConfiguration sub = config.configurationAt("element2", true);
+        SubnodeConfiguration subsub = sub.configurationAt("subelement", true);
+        SubnodeConfiguration sub2 =
+                config.configurationAt("element2.subelement", true);
+        sub.clearTree("subelement");
+        // 3 x configurationAt(), 1 x clearTree()
+        sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE,
+                Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_WRITE,
+                Methods.END_WRITE, Methods.BEGIN_WRITE, Methods.END_WRITE);
+        assertNull("Sub2 still attached", sub2.getSubnodeKey());
+        assertNull("Subsub still attached", subsub.getSubnodeKey());
+    }
+
+    /**
+     * Tests whether a SubnodeConfiguration's clearAndDetachFromParent() method
+     * is correctly synchronized.
+     */
+    @Test
+    public void testSubnodeClearAndDetachFromParentSynchronized()
+    {
+        SubnodeConfiguration sub = config.configurationAt("element2", true);
+        sub.clearAndDetachFromParent();
+        assertFalse("Node not removed", config.containsKey("element2"));
+        // configurationAt() + clearTree() + containsKey()
+        sync.verify(Methods.BEGIN_WRITE, Methods.END_WRITE,
+                Methods.BEGIN_WRITE, Methods.END_WRITE, Methods.BEGIN_READ,
+                Methods.END_READ);
+    }
 }

Modified: 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java
URL: 
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java?rev=1481154&r1=1481153&r2=1481154&view=diff
==============================================================================
--- 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java
 (original)
+++ 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestHierarchicalConfiguration.java
 Fri May 10 19:16:40 2013
@@ -596,16 +596,13 @@ public class TestHierarchicalConfigurati
     @Test
     public void testImmutableConfigurationAtSupportUpdates()
     {
-        String[] tables2 = new String[tables.length];
-        for(int i = 0; i < tables2.length; i++)
-        {
-            tables2[i] = tables[i] + "_other";
-        }
+        String newTableName = tables[1] + "_other";
         ImmutableHierarchicalConfiguration subConfig =
                 config.immutableConfigurationAt("tables.table(1)", true);
-        config.clear();
-        fillConfiguration(tables2);
-        assertEquals("Name not updated", tables2[1], 
subConfig.getString("name"));
+        config.addProperty("tables.table(-1).name", newTableName);
+        config.clearTree("tables.table(1)");
+        assertEquals("Name not updated", newTableName,
+                subConfig.getString("name"));
     }
 
     /**
@@ -1044,7 +1041,9 @@ public class TestHierarchicalConfigurati
     @Test
     public void testInitCopyNull()
     {
-        BaseHierarchicalConfiguration copy = new 
BaseHierarchicalConfiguration(null);
+        BaseHierarchicalConfiguration copy =
+                new BaseHierarchicalConfiguration(
+                        (HierarchicalConfiguration) null);
         assertTrue("Configuration not empty", copy.isEmpty());
     }
 

Modified: 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java
URL: 
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java?rev=1481154&r1=1481153&r2=1481154&view=diff
==============================================================================
--- 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java
 (original)
+++ 
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/TestSubnodeConfiguration.java
 Fri May 10 19:16:40 2013
@@ -21,8 +21,6 @@ import static org.junit.Assert.assertNul
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
-import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -34,13 +32,10 @@ import org.apache.commons.configuration.
 import org.apache.commons.configuration.event.ConfigurationListener;
 import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
 import org.apache.commons.configuration.interpol.Lookup;
-import org.apache.commons.configuration.io.FileHandler;
 import org.apache.commons.configuration.tree.ConfigurationNode;
 import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 
 /**
  * Test case for SubnodeConfiguration.
@@ -65,9 +60,8 @@ public class TestSubnodeConfiguration
     /** Constant for an updated table name.*/
     private static final String NEW_TABLE_NAME = "newTable";
 
-    /** A helper object for creating temporary files. */
-    @Rule
-    public TemporaryFolder folder = new TemporaryFolder();
+    /** The key used for the SubnodeConfiguration. */
+    private static final String SUB_KEY = "tables.table(1)";
 
     /** The parent configuration. */
     private BaseHierarchicalConfiguration parent;
@@ -104,7 +98,7 @@ public class TestSubnodeConfiguration
     @Test(expected = IllegalArgumentException.class)
     public void testInitSubNodeConfigWithNullParent()
     {
-        config = new SubnodeConfiguration(null, getSubnodeRoot(parent));
+        config = new SubnodeConfiguration(null, getSubnodeRoot(parent), null);
     }
 
     /**
@@ -114,7 +108,7 @@ public class TestSubnodeConfiguration
     @Test(expected = IllegalArgumentException.class)
     public void testInitSubNodeConfigWithNullNode()
     {
-        config = new SubnodeConfiguration(parent, null);
+        config = new SubnodeConfiguration(parent, null, null);
     }
 
     /**
@@ -390,7 +384,7 @@ public class TestSubnodeConfiguration
     @Test
     public void testParentReloadNotSupported() throws ConfigurationException
     {
-        Configuration c = setUpReloadTest(false);
+        Configuration c = setUpLiveUpdateTest(false);
         assertEquals("Name changed in sub config", TABLE_NAMES[1], config
                 .getString("name"));
         assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
@@ -404,7 +398,7 @@ public class TestSubnodeConfiguration
     @Test
     public void testParentReloadSupported() throws ConfigurationException
     {
-        Configuration c = setUpReloadTest(true);
+        Configuration c = setUpLiveUpdateTest(true);
         assertEquals("Name not changed in sub config", NEW_TABLE_NAME, config
                 .getString("name"));
         assertEquals("Name not changed in parent", NEW_TABLE_NAME, c
@@ -417,11 +411,11 @@ public class TestSubnodeConfiguration
     @Test
     public void testParentReloadEvents() throws ConfigurationException
     {
-        setUpReloadTest(true);
+        config = parent.configurationAt(SUB_KEY, true);
         ConfigurationListenerTestImpl l = new ConfigurationListenerTestImpl();
         config.addConfigurationListener(l);
-        config.getString("name");
-        assertEquals("Wrong number of events", 2, l.events.size());
+        updateParent();
+        assertEquals("Wrong number of events", 4, l.events.size());
         boolean before = true;
         for (ConfigurationEvent e : l.events)
         {
@@ -445,7 +439,7 @@ public class TestSubnodeConfiguration
     public void testParentReloadSupportAccessParent()
             throws ConfigurationException
     {
-        Configuration c = setUpReloadTest(true);
+        Configuration c = setUpLiveUpdateTest(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
@@ -458,7 +452,7 @@ public class TestSubnodeConfiguration
     @Test
     public void testParentReloadSubSubnode() throws ConfigurationException
     {
-        setUpReloadTest(true);
+        setUpLiveUpdateTest(true);
         SubnodeConfiguration sub = config.configurationAt("fields", true);
         assertEquals("Wrong subnode key", "tables.table(1).fields", sub
                 .getSubnodeKey());
@@ -474,7 +468,7 @@ public class TestSubnodeConfiguration
     public void testParentReloadSubSubnodeNoChangeSupport()
             throws ConfigurationException
     {
-        setUpReloadTest(false);
+        setUpLiveUpdateTest(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
@@ -482,44 +476,39 @@ public class TestSubnodeConfiguration
     }
 
     /**
-     * Prepares a test for a reload operation.
+     * Prepares a test for updates of a SubnodeConfiguration if the node
+     * structure of the parent changes. This method replaces the nodes for the
+     * tables with new ones.
      *
-     * @param supportReload a flag whether the subnode configuration should
-     * support reload operations
+     * @param supportReload a flag whether the SubnodeConfiguration 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
+    private HierarchicalConfiguration setUpLiveUpdateTest(boolean 
supportReload)
     {
-        try
-        {
-            File testFile = folder.newFile();
-            XMLConfiguration xmlConf = new XMLConfiguration(parent);
-            FileHandler handler = new FileHandler(xmlConf);
-            handler.setFile(testFile);
-            handler.save();
-            config = xmlConf.configurationAt("tables.table(1)", supportReload);
-            assertEquals("Wrong table name", TABLE_NAMES[1],
-                    config.getString("name"));
-            // Now change the configuration file
-            XMLConfiguration confUpdate = new XMLConfiguration();
-            FileHandler handler2 = new FileHandler(confUpdate);
-            handler2.setFile(testFile);
-            handler2.load();
-            confUpdate.setProperty("tables.table(1).name", NEW_TABLE_NAME);
-            confUpdate.setProperty("tables.table(1).fields.field(0).name",
-                    "newField");
-            handler2.save();
-            // Reload parent configuration
-            xmlConf.clear();
-            handler.load();
-            return xmlConf;
-        }
-        catch (IOException ioex)
+        config = parent.configurationAt(SUB_KEY, supportReload);
+        updateParent();
+        return parent;
+    }
+
+    /**
+     * Updates the parent configuration. Replaces the node structure so that
+     * an attached SubnodeConfiguration should be removed now.
+     */
+    private void updateParent()
+    {
+        String[] tableNamesNew = TABLE_NAMES.clone();
+        String[][] fieldNamesNew = new String[TABLE_FIELDS.length][];
+        for(int i = 0; i < TABLE_FIELDS.length; i++)
         {
-            throw new ConfigurationException(ioex);
+            fieldNamesNew[i] = TABLE_FIELDS[i].clone();
         }
+        tableNamesNew[1] = NEW_TABLE_NAME;
+        fieldNamesNew[1][0] = "newField";
+        addTableData(parent, tableNamesNew, fieldNamesNew);
+        String keyClear = "tables.table(0)";
+        parent.clearTree(keyClear);
+        parent.clearTree(keyClear);
     }
 
     /**
@@ -530,7 +519,7 @@ public class TestSubnodeConfiguration
     @Test
     public void testParentChangeDetach()
     {
-        final String key = "tables.table(1)";
+        final String key = SUB_KEY;
         config = parent.configurationAt(key, true);
         assertEquals("Wrong subnode key", key, config.getSubnodeKey());
         assertEquals("Wrong table name", TABLE_NAMES[1], config
@@ -549,8 +538,9 @@ public class TestSubnodeConfiguration
     @Test
     public void testParentChangeDetatchException()
     {
-        config = parent.configurationAt("tables.table(1)", true);
+        config = parent.configurationAt(SUB_KEY, true);
         parent.setExpressionEngine(new XPathExpressionEngine());
+        parent.addProperty("newProp", "value");
         assertEquals("Wrong name of table", TABLE_NAMES[1], config
                 .getString("name"));
         assertNull("Sub config was not detached", config.getSubnodeKey());
@@ -580,16 +570,30 @@ public class TestSubnodeConfiguration
                 return super.createNode(name);
             }
         };
-        for (int i = 0; i < TABLE_NAMES.length; i++)
+        addTableData(conf, TABLE_NAMES, TABLE_FIELDS);
+        return conf;
+    }
+
+    /**
+     * Appends properties for table names and their fields to the given
+     * configuration.
+     *
+     * @param conf the configuration to be filled
+     * @param tableNames an array with the names of the tables to add
+     * @param fields and array with the field names per table
+     */
+    private static void addTableData(Configuration conf, String[] tableNames,
+            String[][] fields)
+    {
+        for (int i = 0; i < tableNames.length; i++)
         {
-            conf.addProperty("tables.table(-1).name", TABLE_NAMES[i]);
-            for (int j = 0; j < TABLE_FIELDS[i].length; j++)
+            conf.addProperty("tables.table(-1).name", tableNames[i]);
+            for (int j = 0; j < fields[i].length; j++)
             {
                 conf.addProperty("tables.table.fields.field(-1).name",
-                        TABLE_FIELDS[i][j]);
+                        fields[i][j]);
             }
         }
-        return conf;
     }
 
     /**
@@ -610,7 +614,7 @@ public class TestSubnodeConfiguration
      */
     protected void setUpSubnodeConfig()
     {
-        config = new SubnodeConfiguration(parent, getSubnodeRoot(parent));
+        config = new SubnodeConfiguration(parent, getSubnodeRoot(parent), 
null);
     }
 
     /**


Reply via email to