You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by oh...@apache.org on 2014/02/16 22:17:39 UTC

svn commit: r1568830 - in /commons/proper/configuration/branches/immutableNodes/src: main/java/org/apache/commons/configuration/tree/ test/java/org/apache/commons/configuration/tree/

Author: oheger
Date: Sun Feb 16 21:17:39 2014
New Revision: 1568830

URL: http://svn.apache.org/r1568830
Log:
Added a clearTree() method to InMemoryNodeModel.

With this method whole subtrees for the managed node structure can be removed.
The current implementation is still incomplete.

Modified:
    commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/InMemoryNodeModel.java
    commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/ModelTransaction.java
    commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModel.java

Modified: commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/InMemoryNodeModel.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/InMemoryNodeModel.java?rev=1568830&r1=1568829&r2=1568830&view=diff
==============================================================================
--- commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/InMemoryNodeModel.java (original)
+++ commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/InMemoryNodeModel.java Sun Feb 16 21:17:39 2014
@@ -178,8 +178,7 @@ public class InMemoryNodeModel implement
      */
     public boolean isDefined(ImmutableNode node)
     {
-        return node.getValue() != null || !node.getChildren().isEmpty()
-                || !node.getAttributes().isEmpty();
+        return checkIfNodeDefined(node);
     }
 
     /**
@@ -216,6 +215,31 @@ public class InMemoryNodeModel implement
     }
 
     /**
+     * Removes the sub trees defined by the given key from this model. All nodes
+     * selected by this key are retrieved and removed from the model. If this
+     * causes other nodes to become undefined, they are removed, too.
+     *
+     * @param key the key selecting the properties to be removed
+     * @param resolver the {@code NodeKeyResolver}
+     */
+    public void clearTree(String key, NodeKeyResolver resolver)
+    {
+        TreeData currentStructure = structure.get();
+        ModelTransaction tx = new ModelTransaction(currentStructure);
+        for (QueryResult<ImmutableNode> result : resolver.resolveKey(
+                currentStructure.getRoot(), key, this))
+        {
+            // TODO handle attributes
+            tx.addRemoveNodeOperation(
+                    currentStructure.getParent(result.getNode()),
+                    result.getNode());
+        }
+
+        // TODO handle concurrency
+        structure.set(tx.execute());
+    }
+
+    /**
      * Updates the mapping from nodes to their parents for the passed in
      * hierarchy of nodes. This method traverses all children and grand-children
      * of the passed in root node. For each node in the subtree the parent
@@ -242,6 +266,19 @@ public class InMemoryNodeModel implement
     }
 
     /**
+     * Checks if the passed in node is defined. Result is <b>true</b> if the
+     * node contains any data.
+     *
+     * @param node the node in question
+     * @return <b>true</b> if the node is defined, <b>false</b> otherwise
+     */
+    static boolean checkIfNodeDefined(ImmutableNode node)
+    {
+        return node.getValue() != null || !node.getChildren().isEmpty()
+                || !node.getAttributes().isEmpty();
+    }
+
+    /**
      * Handles an add property operation if the property to be added is a node.
      *
      * @param tx the transaction

Modified: commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/ModelTransaction.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/ModelTransaction.java?rev=1568830&r1=1568829&r2=1568830&view=diff
==============================================================================
--- commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/ModelTransaction.java (original)
+++ commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/ModelTransaction.java Sun Feb 16 21:17:39 2014
@@ -20,9 +20,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
@@ -77,6 +79,9 @@ class ModelTransaction
     /** A collection with nodes which have been added. */
     private final Collection<ImmutableNode> addedNodes;
 
+    /** A collection with nodes which have been removed. */
+    private final Collection<ImmutableNode> removedNodes;
+
     /**
      * Stores the operations to be executed during this transaction. The map is
      * sorted by the levels of the nodes to be manipulated: Operations on nodes
@@ -101,6 +106,7 @@ class ModelTransaction
         parentMapping = data.copyParentMapping();
         operations = new TreeMap<Integer, Map<ImmutableNode, Operations>>();
         addedNodes = new LinkedList<ImmutableNode>();
+        removedNodes = new LinkedList<ImmutableNode>();
     }
 
     /**
@@ -146,6 +152,19 @@ class ModelTransaction
     }
 
     /**
+     * Adds an operation for removing a child node of a given node.
+     *
+     * @param parent the parent node
+     * @param node the child node to be removed
+     */
+    public void addRemoveNodeOperation(ImmutableNode parent, ImmutableNode node)
+    {
+        ChildrenUpdateOperation op = new ChildrenUpdateOperation();
+        op.addNodeToRemove(node);
+        fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op);
+    }
+
+    /**
      * Executes this transaction resulting in a new {@code TreeData} object. The
      * object returned by this method serves as the definition of a new node
      * structure for the calling model.
@@ -240,11 +259,20 @@ class ModelTransaction
     /**
      * Updates the parent mapping for the resulting {@code TreeData} instance.
      * This method is called after all update operations have been executed. It
-     * ensures that the parent mapping is updated for newly added node
-     * structures.
+     * ensures that the parent mapping is updated for the changes on the nodes
+     * structure.
      */
     private void updateParentMapping()
     {
+        updateParentMappingForAddedNodes();
+        updateParentMappingForRemovedNodes();
+    }
+
+    /**
+     * Adds newly added nodes and their children to the parent mapping.
+     */
+    private void updateParentMappingForAddedNodes()
+    {
         for (ImmutableNode node : addedNodes)
         {
             InMemoryNodeModel.updateParentMapping(parentMapping, node);
@@ -252,6 +280,57 @@ class ModelTransaction
     }
 
     /**
+     * Removes nodes that have been removed during this transaction from the
+     * parent and replacement mappings.
+     */
+    private void updateParentMappingForRemovedNodes()
+    {
+        for (ImmutableNode node : removedNodes)
+        {
+            removeNodesFromParentAndReplacementMapping(node);
+        }
+    }
+
+    /**
+     * Removes a node and its children (recursively) from the parent and the
+     * replacement mappings.
+     *
+     * @param root the root of the subtree to be removed
+     */
+    private void removeNodesFromParentAndReplacementMapping(ImmutableNode root)
+    {
+        List<ImmutableNode> pendingNodes = new LinkedList<ImmutableNode>();
+        pendingNodes.add(root);
+
+        while (!pendingNodes.isEmpty())
+        {
+            ImmutableNode node = pendingNodes.remove(0);
+            parentMapping.remove(node);
+            removeNodeFromReplacementMapping(node);
+            for (ImmutableNode c : node.getChildren())
+            {
+                pendingNodes.add(c);
+            }
+        }
+
+    }
+
+    /**
+     * Removes the specified node completely from the replacement mapping. This
+     * also includes the nodes that replace the given one.
+     *
+     * @param node the node to be removed
+     */
+    private void removeNodeFromReplacementMapping(ImmutableNode node)
+    {
+        ImmutableNode replacement = node;
+        do
+        {
+            replacement = replacedNodes.remove(replacement);
+        } while (replacement != null);
+    }
+
+    /**
      * Constructs the concatenation of two collections. Both can be null.
      *
      * @param col1 the first collection
@@ -274,6 +353,26 @@ class ModelTransaction
     }
 
     /**
+     * Constructs the concatenation of two sets. Both can be null.
+     *
+     * @param set1 the first set
+     * @param set2 the second set
+     * @param <E> the type of the elements involved
+     * @return the resulting set
+     */
+    private static <E> Set<E> concatenate(Set<E> set1, Set<? extends E> set2)
+    {
+        if (set2 == null)
+        {
+            return set1;
+        }
+
+        Set<E> result = (set1 != null) ? set1 : new HashSet<E>();
+        result.addAll(set2);
+        return result;
+    }
+
+    /**
      * Constructs the concatenation of two maps. Both can be null.
      *
      * @param map1 the first map
@@ -312,6 +411,22 @@ class ModelTransaction
     }
 
     /**
+     * Appends a single element to a set. The set may be null then it is
+     * created.
+     *
+     * @param col the set
+     * @param elem the element to be added
+     * @param <E> the type of the elements involved
+     * @return the resulting set
+     */
+    private static <E> Set<E> append(Set<E> col, E elem)
+    {
+        Set<E> result = (col != null) ? col : new HashSet<E>();
+        result.add(elem);
+        return result;
+    }
+
+    /**
      * Adds a single key-value pair to a map. The map may be null, then it is
      * created.
      *
@@ -361,6 +476,9 @@ class ModelTransaction
         /** A collection with new nodes to be added. */
         private Collection<ImmutableNode> newNodes;
 
+        /** A collection with nodes to be removed. */
+        private Set<ImmutableNode> nodesToRemove;
+
         /**
          * A map with nodes to be replaced by others. The keys are the nodes to
          * be replaced, the values the replacements.
@@ -376,6 +494,7 @@ class ModelTransaction
         {
             newNodes = concatenate(newNodes, op.newNodes);
             nodesToReplace = concatenate(nodesToReplace, op.nodesToReplace);
+            nodesToRemove = concatenate(nodesToRemove, op.nodesToRemove);
         }
 
         /**
@@ -413,6 +532,17 @@ class ModelTransaction
         }
 
         /**
+         * Adds a node for a remove operation. This child node is going to be
+         * removed from its parent.
+         *
+         * @param node the child node to be removed
+         */
+        public void addNodeToRemove(ImmutableNode node)
+        {
+            nodesToRemove = append(nodesToRemove, node);
+        }
+
+        /**
          * {@inheritDoc} This implementation applies changes on the children of
          * the passed in target node according to its configuration: new nodes
          * are added, replacements are performed, and nodes no longer needed are
@@ -422,15 +552,9 @@ class ModelTransaction
         protected ImmutableNode apply(ImmutableNode target,
                 Operations operations)
         {
-            Map<ImmutableNode, ImmutableNode> replacements;
-            if (nodesToReplace == null)
-            {
-                replacements = Collections.emptyMap();
-            }
-            else
-            {
-                replacements = nodesToReplace;
-            }
+            Map<ImmutableNode, ImmutableNode> replacements =
+                    fetchReplacementMap();
+            Set<ImmutableNode> removals = fetchRemovalSet();
             List<ImmutableNode> resultNodes = new LinkedList<ImmutableNode>();
 
             for (ImmutableNode nd : target.getChildren())
@@ -443,7 +567,14 @@ class ModelTransaction
                 }
                 else
                 {
-                    resultNodes.add(nd);
+                    if (removals.contains(nd))
+                    {
+                        removedNodes.add(nd);
+                    }
+                    else
+                    {
+                        resultNodes.add(nd);
+                    }
                 }
             }
 
@@ -451,6 +582,30 @@ class ModelTransaction
             operations.newNodesAdded(newNodes);
             return target.replaceChildren(resultNodes);
         }
+
+        /**
+         * Obtains the map with replacement nodes. If no replacements are
+         * defined, an empty map is returned.
+         *
+         * @return the map with replacement nodes
+         */
+        private Map<ImmutableNode, ImmutableNode> fetchReplacementMap()
+        {
+            return (nodesToReplace != null) ? nodesToReplace : Collections
+                    .<ImmutableNode, ImmutableNode> emptyMap();
+        }
+
+        /**
+         * Returns a set with nodes to be removed. If no remove operations are
+         * pending, an empty set is returned.
+         *
+         * @return the set with nodes to be removed
+         */
+        private Set<ImmutableNode> fetchRemovalSet()
+        {
+            return (nodesToRemove != null) ? nodesToRemove : Collections
+                    .<ImmutableNode> emptySet();
+        }
     }
 
     /**
@@ -578,11 +733,33 @@ class ModelTransaction
             else
             {
                 // propagate change
-                ImmutableNode parent = getParent(target);
-                ChildrenUpdateOperation co = new ChildrenUpdateOperation();
+                propagateChange(target, node, level);
+            }
+        }
+
+        /**
+         * Propagates the changes on the target node to the next level above of
+         * the hierarchy. If the updated node is no longer defined, it can even
+         * be removed from its parent. Otherwise, it is just replaced.
+         *
+         * @param target the target node for this operation
+         * @param node the resulting node after applying all operations
+         * @param level the level of the target node
+         */
+        private void propagateChange(ImmutableNode target, ImmutableNode node,
+                int level)
+        {
+            ImmutableNode parent = getParent(target);
+            ChildrenUpdateOperation co = new ChildrenUpdateOperation();
+            if (InMemoryNodeModel.checkIfNodeDefined(node))
+            {
                 co.addNodeToReplace(target, node);
-                fetchOperations(parent, level - 1).addChildrenOperation(co);
             }
+            else
+            {
+                co.addNodeToRemove(target);
+            }
+            fetchOperations(parent, level - 1).addChildrenOperation(co);
         }
 
         /**

Modified: commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModel.java
URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModel.java?rev=1568830&r1=1568829&r2=1568830&view=diff
==============================================================================
--- commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModel.java (original)
+++ commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModel.java Sun Feb 16 21:17:39 2014
@@ -18,11 +18,14 @@ package org.apache.commons.configuration
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -662,8 +665,45 @@ public class TestInMemoryNodeModel
                 "Homer", "Ilias", "locations"
         };
         ImmutableNode node =
-                nodeForKey(model, StringUtils.join(path, PATH_SEPARATOR)
-                        + "/location(1)");
+                nodeForKey(model, nodePathWithEndNode("location(1)", path));
+        checkPathToRoot(model, node, path);
+    }
+
+    /**
+     * Convenience method for creating a path for accessing a node based on the
+     * node names.
+     *
+     * @param path an array with the expected node names on the path
+     * @return the resulting path as string
+     */
+    private static String nodePath(String... path)
+    {
+        return StringUtils.join(path, PATH_SEPARATOR);
+    }
+
+    /**
+     * Convenience method for creating a node path with a special end node.
+     *
+     * @param endNode the name of the last path component
+     * @param path an array with the expected node names on the path
+     * @return the resulting path as string
+     */
+    private static String nodePathWithEndNode(String endNode, String... path)
+    {
+        return nodePath(path) + PATH_SEPARATOR + endNode;
+    }
+
+    /**
+     * Helper method for checking whether the expected nodes are encountered on
+     * a path from a start node to the root node.
+     *
+     * @param model the node model
+     * @param node the start node in the path
+     * @param path an array with the expected node names on the path
+     */
+    private static void checkPathToRoot(InMemoryNodeModel model,
+            ImmutableNode node, String... path)
+    {
         for (int i = path.length - 1; i >= 0; i--)
         {
             node = model.getParent(node);
@@ -749,8 +789,163 @@ public class TestInMemoryNodeModel
         EasyMock.replay(resolver);
         InMemoryNodeModel model = new InMemoryNodeModel(rootAuthorsTree);
 
-        model.addProperty(KEY, Collections.<Object> emptySet(), resolver);
+        model.addProperty(KEY, Collections.emptySet(), resolver);
         assertSame("Root node was changed", rootAuthorsTree,
                 model.getRootNode());
     }
+
+    /**
+     * Tests whether a clearTree() operation can be performed if only nodes are
+     * involved.
+     */
+    @Test
+    public void testClearTreeNodes()
+    {
+        NodeKeyResolver resolver = EasyMock.createMock(NodeKeyResolver.class);
+        InMemoryNodeModel model = new InMemoryNodeModel(rootAuthorsTree);
+        QueryResult<ImmutableNode> result =
+                QueryResult.createNodeResult(nodeForKey(model,
+                        "Homer/Ilias/Achilles"));
+        EasyMock.expect(resolver.resolveKey(rootAuthorsTree, KEY, model))
+                .andReturn(Collections.singletonList(result));
+        EasyMock.replay(resolver);
+
+        model.clearTree(KEY, resolver);
+        ImmutableNode node = nodeForKey(model, "Homer/Ilias");
+        assertEquals("Wrong number of children", 2, node.getChildren().size());
+        for (ImmutableNode c : node.getChildren())
+        {
+            assertNotEquals("Node still found", result.getNode().getNodeName(),
+                    c.getNodeName());
+        }
+    }
+
+    /**
+     * Helper method for testing whether nodes removed from the model can no
+     * longer be looked up in the parent mapping.
+     *
+     * @param pathToRemove the path to the node to be removed
+     * @param nodeToCheck the node to check in the parent mapping
+     */
+    private void checkClearTreeUpdatedParentMapping(String pathToRemove,
+            ImmutableNode nodeToCheck)
+    {
+        NodeKeyResolver resolver = EasyMock.createMock(NodeKeyResolver.class);
+        InMemoryNodeModel model = new InMemoryNodeModel(rootAuthorsTree);
+        QueryResult<ImmutableNode> result =
+                QueryResult.createNodeResult(nodeForKey(model, pathToRemove));
+        EasyMock.expect(resolver.resolveKey(rootAuthorsTree, KEY, model))
+                .andReturn(Collections.singletonList(result));
+        EasyMock.replay(resolver);
+
+        model.clearTree(KEY, resolver);
+        try
+        {
+            model.getParent(nodeToCheck);
+            fail("Removed node still in parent mapping!");
+        }
+        catch (IllegalArgumentException iaex)
+        {
+            // expected result
+        }
+    }
+
+    /**
+     * Tests whether a removed node can no longer be passed to getParent().
+     */
+    @Test
+    public void testClearTreeNodeRemovedFromParentMapping()
+    {
+        String path = "Homer/Ilias/Achilles";
+        checkClearTreeUpdatedParentMapping(path,
+                nodeForKey(rootAuthorsTree, path));
+    }
+
+    /**
+     * Tests whether the children of removed nodes are also removed from the
+     * parent mapping.
+     */
+    @Test
+    public void testClearTreeChildrenRemovedFromParentMapping()
+    {
+        String path = "Homer/Ilias";
+        checkClearTreeUpdatedParentMapping(path,
+                nodeForKey(rootAuthorsTree, path + "/Achilles"));
+    }
+
+    /**
+     * Tests whether references to parent nodes are updated correctly when
+     * clearing properties.
+     */
+    @Test
+    public void testClearTreeUpdateParentReferences()
+    {
+        String[] path = {
+                "Homer", "Ilias"
+        };
+        NodeKeyResolver resolver = EasyMock.createMock(NodeKeyResolver.class);
+        InMemoryNodeModel model = new InMemoryNodeModel(rootAuthorsTree);
+        QueryResult<ImmutableNode> result =
+                QueryResult.createNodeResult(nodeForKey(model,
+                        nodePathWithEndNode("Achilles", path)));
+        EasyMock.expect(resolver.resolveKey(rootAuthorsTree, KEY, model))
+                .andReturn(Collections.singletonList(result));
+        EasyMock.replay(resolver);
+
+        model.clearTree(KEY, resolver);
+        checkPathToRoot(model,
+                nodeForKey(model, nodePathWithEndNode("Hektor", path)), path);
+    }
+
+    /**
+     * Tests whether undefined nodes are removed from the hierarchy when
+     * clearing properties.
+     */
+    @Test
+    public void testClearTreeRemoveUndefinedNodes()
+    {
+        NodeKeyResolver resolver = EasyMock.createMock(NodeKeyResolver.class);
+        InMemoryNodeModel model = new InMemoryNodeModel(rootAuthorsTree);
+        ImmutableNode node = nodeForKey(model, "Homer/Ilias");
+        List<QueryResult<ImmutableNode>> results =
+                new ArrayList<QueryResult<ImmutableNode>>(node.getChildren()
+                        .size());
+        for (ImmutableNode child : node.getChildren())
+        {
+            results.add(QueryResult.createNodeResult(child));
+        }
+        EasyMock.expect(resolver.resolveKey(rootAuthorsTree, KEY, model))
+                .andReturn(results);
+        EasyMock.replay(resolver);
+
+        model.clearTree(KEY, resolver);
+        assertEquals("Child of root not removed", AUTHORS.length - 1, model
+                .getRootNode().getChildren().size());
+        for (ImmutableNode child : model.getRootNode().getChildren())
+        {
+            assertNotEquals("Child still found", "Homer", child.getNodeName());
+        }
+    }
+
+    /**
+     * Tests a clearTree() operation which should yield an empty tree structure.
+     */
+    @Test
+    public void testClearTreeResultIsEmpty()
+    {
+        NodeKeyResolver resolver = EasyMock.createMock(NodeKeyResolver.class);
+        ImmutableNode child =
+                new ImmutableNode.Builder().name("child").value("test")
+                        .create();
+        ImmutableNode root =
+                new ImmutableNode.Builder(1).addChild(child).create();
+        InMemoryNodeModel model = new InMemoryNodeModel(root);
+        EasyMock.expect(resolver.resolveKey(root, KEY, model)).andReturn(
+                Collections.singletonList(QueryResult.createNodeResult(child)));
+        EasyMock.replay(resolver);
+
+        model.clearTree(KEY, resolver);
+        assertFalse("Root node still defined",
+                model.isDefined(model.getRootNode()));
+    }
 }