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()));
+ }
}