Author: oheger Date: Sun Mar 9 20:56:06 2014 New Revision: 1575756 URL: http://svn.apache.org/r1575756 Log: InMemoryNodeModel now provides support for tracking nodes.
Nodes can be tracked by specifying a corresponding NodeSelector. After updates of the model, the node instances associated with this key are still available. Added: commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModelTrackedNodes.java 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 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=1575756&r1=1575755&r2=1575756&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 Mar 9 20:56:06 2014 @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.configuration.ex.ConfigurationRuntimeException; import org.apache.commons.lang3.StringUtils; /** @@ -75,7 +76,7 @@ public class InMemoryNodeModel implement { structure = new AtomicReference<TreeData>( - createTreeData(initialRootNode(root))); + createTreeData(initialRootNode(root), null)); } public ImmutableNode getRootNode() @@ -197,7 +198,7 @@ public class InMemoryNodeModel implement initializeAddTransaction(tx, key, values, resolver); return true; } - }); + }, resolver); } } @@ -240,7 +241,7 @@ public class InMemoryNodeModel implement } return true; } - }); + }, resolver); } } @@ -270,7 +271,7 @@ public class InMemoryNodeModel implement updateData.getChangedValues()); return added || cleared || updated; } - }); + }, resolver); } /** @@ -310,7 +311,7 @@ public class InMemoryNodeModel implement } return true; } - }); + }, resolver); } /** @@ -327,7 +328,7 @@ public class InMemoryNodeModel implement initializeClearTransaction(tx, results); return true; } - }); + }, resolver); } /** @@ -341,9 +342,7 @@ public class InMemoryNodeModel implement ImmutableNode newRoot = new ImmutableNode.Builder().name(getRootNode().getNodeName()) .create(); - structure.set(new TreeData(newRoot, Collections - .<ImmutableNode, ImmutableNode>emptyMap(), Collections - .<ImmutableNode, ImmutableNode>emptyMap())); + setRootNode(newRoot); } /** @@ -356,7 +355,78 @@ public class InMemoryNodeModel implement */ public void setRootNode(ImmutableNode newRoot) { - structure.set(createTreeData(initialRootNode(newRoot))); + structure.set(createTreeData(initialRootNode(newRoot), structure.get())); + } + + /** + * Adds a node to be tracked. After this method has been called with a + * specific {@code NodeSelector}, the node associated with this key can be + * always obtained using {@link #getTrackedNode(NodeSelector)} with the same + * selector. This is useful because during updates of a model parts of the + * structure are replaced. Therefore, it is not a good idea to simply hold a + * reference to a node; this might become outdated soon. Rather, the node + * should be tracked. This mechanism ensures that always the correct node + * reference can be obtained. + * + * @param selector the {@code NodeSelector} defining the desired node + * @param resolver the {@code NodeKeyResolver} + * @throws ConfigurationRuntimeException if the selector does not select a + * single node + */ + public void trackNode(NodeSelector selector, + NodeKeyResolver<ImmutableNode> resolver) + { + boolean done; + do + { + TreeData current = structure.get(); + NodeTracker newTracker = + current.getNodeTracker().trackNode(current.getRoot(), + selector, resolver, this); + done = + structure.compareAndSet(current, + current.updateNodeTracker(newTracker)); + } while (!done); + } + + /** + * Returns the current {@code ImmutableNode} instance associated with the + * given {@code NodeSelector}. The node must be a tracked node, i.e. + * {@link #trackNode(NodeSelector, NodeKeyResolver)} must have been called + * before with the given selector. + * + * @param selector the {@code NodeSelector} defining the desired node + * @return the current {@code ImmutableNode} associated with this selector + * @throws ConfigurationRuntimeException if the selector is unknown + */ + public ImmutableNode getTrackedNode(NodeSelector selector) + { + return structure.get().getNodeTracker().getTrackedNode(selector); + } + + /** + * Removes a tracked node. This method is the opposite of + * {@code trackNode()}. It has to be called if there is no longer the need + * to track a specific node. Note that for each call of {@code trackNode()} + * there has to be a corresponding {@code untrackNode()} call. This ensures + * that multiple observers can track the same node. + * + * @param selector the {@code NodeSelector} defining the desired node + * @throws ConfigurationRuntimeException if the specified node is not + * tracked + */ + public void untrackNode(NodeSelector selector) + { + boolean done; + do + { + TreeData current = structure.get(); + NodeTracker newTracker = + current.getNodeTracker().untrackNode(selector); + done = + structure.compareAndSet(current, + current.updateNodeTracker(newTracker)); + } while (!done); } /** @@ -436,12 +506,17 @@ public class InMemoryNodeModel implement * Creates a {@code TreeData} object for the specified root node. * * @param root the root node of the current tree + * @param current the current {@code TreeData} object (may be <b>null</b>) * @return the {@code TreeData} describing the current tree */ - private TreeData createTreeData(ImmutableNode root) + private TreeData createTreeData(ImmutableNode root, TreeData current) { + NodeTracker newTracker = + (current != null) ? current.getNodeTracker() + : new NodeTracker(); return new TreeData(root, createParentMapping(root), - new HashMap<ImmutableNode, ImmutableNode>()); + Collections.<ImmutableNode, ImmutableNode> emptyMap(), + newTracker); } /** @@ -675,14 +750,16 @@ public class InMemoryNodeModel implement * update was successful even if the model is concurrently accessed. * * @param txInit the {@code TransactionInitializer} + * @param resolver the {@code NodeKeyResolver} */ - private void updateModel(TransactionInitializer txInit) + private void updateModel(TransactionInitializer txInit, + NodeKeyResolver<ImmutableNode> resolver) { boolean done; do { - ModelTransaction tx = new ModelTransaction(this); + ModelTransaction tx = new ModelTransaction(this, resolver); if (!txInit.initTransaction(tx)) { done = true; @@ -744,6 +821,9 @@ public class InMemoryNodeModel implement /** An inverse replacement mapping. */ private final Map<ImmutableNode, ImmutableNode> inverseReplacementMapping; + /** The node tracker. */ + private final NodeTracker nodeTracker; + /** * Creates a new instance of {@code TreeData} and initializes it with * all data to be stored. @@ -751,15 +831,17 @@ public class InMemoryNodeModel implement * @param root the root node of the current tree * @param parentMapping the mapping to parent nodes * @param replacements the map with the nodes that have been replaced + * @param tracker the {@code NodeTracker} */ public TreeData(ImmutableNode root, - Map<ImmutableNode, ImmutableNode> parentMapping, - Map<ImmutableNode, ImmutableNode> replacements) + Map<ImmutableNode, ImmutableNode> parentMapping, + Map<ImmutableNode, ImmutableNode> replacements, NodeTracker tracker) { this.root = root; this.parentMapping = parentMapping; replacementMapping = replacements; inverseReplacementMapping = createInverseMapping(replacements); + nodeTracker = tracker; } /** @@ -773,6 +855,16 @@ public class InMemoryNodeModel implement } /** + * Returns the {@code NodeTracker} + * + * @return the {@code NodeTracker} + */ + public NodeTracker getNodeTracker() + { + return nodeTracker; + } + + /** * Returns the parent node of the specified node. Result is <b>null</b> * for the root node. If the passed in node cannot be resolved, an * exception is thrown. @@ -819,6 +911,20 @@ public class InMemoryNodeModel implement } /** + * Creates a new instance which uses the specified {@code NodeTracker}. + * This method is called when there are updates of the state of tracked + * nodes. + * + * @param newTracker the new {@code NodeTracker} + * @return the updated instance + */ + public TreeData updateNodeTracker(NodeTracker newTracker) + { + return new TreeData(root, parentMapping, replacementMapping, + newTracker); + } + + /** * Checks whether the passed in node is subject of a replacement by * another one. If so, the other node is returned. This is done until a * node is found which had not been replaced. Updating the parent 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=1575756&r1=1575755&r2=1575756&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 Mar 9 20:56:06 2014 @@ -83,6 +83,9 @@ class ModelTransaction /** Stores the current tree data of the calling node model. */ private final InMemoryNodeModel.TreeData currentData; + /** The {@code NodeKeyResolver} to be used for this transaction. */ + private final NodeKeyResolver<ImmutableNode> resolver; + /** A new replacement mapping. */ private final Map<ImmutableNode, ImmutableNode> replacedNodes; @@ -111,11 +114,14 @@ class ModelTransaction * data. * * @param nodeModel the owning {@code InMemoryNodeModel} + * @param resolver the {@code NodeKeyResolver} */ - public ModelTransaction(InMemoryNodeModel nodeModel) + public ModelTransaction(InMemoryNodeModel nodeModel, + NodeKeyResolver<ImmutableNode> resolver) { model = nodeModel; currentData = model.getTreeData(); + this.resolver = resolver; replacedNodes = getCurrentData().copyReplacementMapping(); parentMapping = getCurrentData().copyParentMapping(); operations = new TreeMap<Integer, Map<ImmutableNode, Operations>>(); @@ -124,6 +130,16 @@ class ModelTransaction } /** + * Returns the {@code NodeKeyResolver} used by this transaction. + * + * @return the {@code NodeKeyResolver} + */ + public NodeKeyResolver<ImmutableNode> getResolver() + { + return resolver; + } + + /** * Adds an operation for adding a number of new children to a given parent * node. * @@ -225,7 +241,8 @@ class ModelTransaction executeOperations(); updateParentMapping(); return new InMemoryNodeModel.TreeData(newRoot, parentMapping, - replacedNodes); + replacedNodes, currentData.getNodeTracker().update(newRoot, + getResolver(), model)); } /** Added: commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModelTrackedNodes.java URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModelTrackedNodes.java?rev=1575756&view=auto ============================================================================== --- commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModelTrackedNodes.java (added) +++ commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/TestInMemoryNodeModelTrackedNodes.java Sun Mar 9 20:56:06 2014 @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.configuration.tree; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import org.apache.commons.configuration.ex.ConfigurationRuntimeException; +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * A special test class for {@code InMemoryNodeModel} which tests the facilities + * for tracking nodes. + * + * @version $Id$ + */ +public class TestInMemoryNodeModelTrackedNodes +{ + /** The root node for the test hierarchy. */ + private static ImmutableNode root; + + /** A default node selector initialized with a test key. */ + private static NodeSelector selector; + + /** The model to be tested. */ + private InMemoryNodeModel model; + + @BeforeClass + public static void setUpBeforeClass() throws Exception + { + root = + new ImmutableNode.Builder(1).addChild( + NodeStructureHelper.ROOT_TABLES_TREE).create(); + selector = new NodeSelector("tables.table(1)"); + } + + @Before + public void setUp() throws Exception + { + model = new InMemoryNodeModel(root); + } + + /** + * Creates a default resolver which supports arbitrary queries on a target + * node. + * + * @return the resolver + */ + private static NodeKeyResolver<ImmutableNode> createResolver() + { + NodeKeyResolver<ImmutableNode> resolver = + NodeStructureHelper.createResolverMock(); + NodeStructureHelper.expectResolveKeyForQueries(resolver); + EasyMock.replay(resolver); + return resolver; + } + + /** + * Tries to call trackNode() with a key that does not yield any results. + */ + @Test(expected = ConfigurationRuntimeException.class) + public void testTrackNodeKeyNoResults() + { + model.trackNode(new NodeSelector("tables.unknown"), createResolver()); + } + + /** + * Tries to call trackNode() with a key that selects multiple results. + */ + @Test(expected = ConfigurationRuntimeException.class) + public void testTrackNodeKeyMultipleResults() + { + model.trackNode(new NodeSelector("tables.table.fields.field.name"), + createResolver()); + } + + /** + * Tests whether a tracked node can be queried. + */ + @Test + public void testGetTrackedNodeExisting() + { + ImmutableNode node = + NodeStructureHelper.nodeForKey(model, "tables/table(1)"); + model.trackNode(selector, createResolver()); + assertSame("Wrong node", node, model.getTrackedNode(selector)); + } + + /** + * Tries to obtain a tracked node which is unknown. + */ + @Test(expected = ConfigurationRuntimeException.class) + public void testGetTrackedNodeNonExisting() + { + model.getTrackedNode(selector); + } + + /** + * Tests whether a tracked node survives updates of the node model. + */ + @Test + public void testGetTrackedNodeAfterUpdate() + { + NodeKeyResolver<ImmutableNode> resolver = createResolver(); + model.trackNode(selector, resolver); + model.clearProperty("tables.table(1).fields.field(1).name", resolver); + ImmutableNode node = model.getTrackedNode(selector); + assertEquals("Wrong node", NodeStructureHelper.table(1), node + .getChildren().get(0).getValue()); + } + + /** + * Tests whether a tracked node can be queried even if it was removed from + * the structure. + */ + @Test + public void testGetTrackedNodeAfterUpdateNoLongerExisting() + { + ImmutableNode node = + NodeStructureHelper.nodeForKey(model, "tables/table(1)"); + NodeKeyResolver<ImmutableNode> resolver = createResolver(); + model.trackNode(selector, resolver); + model.clearTree("tables.table(0)", resolver); + assertSame("Wrong node", node, model.getTrackedNode(selector)); + } + + /** + * Tests whether a tracked node can be queried even after the model was + * cleared. + */ + @Test + public void testGetTrackedNodeAfterClear() + { + ImmutableNode node = + NodeStructureHelper.nodeForKey(model, "tables/table(1)"); + NodeKeyResolver<ImmutableNode> resolver = createResolver(); + model.trackNode(selector, resolver); + model.clear(); + assertSame("Wrong node", node, model.getTrackedNode(selector)); + } + + /** + * Tests whether a tracked node can be queried after the root node was + * changed. + */ + @Test + public void testGetTrackedNodeAfterSetRootNode() + { + ImmutableNode node = + NodeStructureHelper.nodeForKey(model, "tables/table(1)"); + NodeKeyResolver<ImmutableNode> resolver = createResolver(); + model.trackNode(selector, resolver); + model.setRootNode(root); + assertSame("Wrong node", node, model.getTrackedNode(selector)); + } + + /** + * Tries to stop tracking of a node which is not tracked. + */ + @Test(expected = ConfigurationRuntimeException.class) + public void testUntrackNodeNonExisting() + { + model.untrackNode(selector); + } + + /** + * Tests whether tracking of a node can be stopped. + */ + @Test + public void testUntrackNode() + { + model.trackNode(selector, createResolver()); + model.untrackNode(selector); + try + { + model.getTrackedNode(selector); + fail("Could get untracked node!"); + } + catch (ConfigurationRuntimeException crex) + { + // expected + } + } + + /** + * Tests whether a single node can be tracked multiple times. + */ + @Test + public void testTrackNodeMultipleTimes() + { + NodeKeyResolver<ImmutableNode> resolver = createResolver(); + model.trackNode(selector, resolver); + model.trackNode(selector, resolver); + model.untrackNode(selector); + assertNotNull("No tracked node", model.getTrackedNode(selector)); + } +}