Author: oheger Date: Sun Mar 9 20:55:21 2014 New Revision: 1575755 URL: http://svn.apache.org/r1575755 Log: Added a helper class for tracking nodes.
This class allows access to nodes by key even if the nodes structure was changed by update operations. Added: commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/NodeTracker.java Added: commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/NodeTracker.java URL: http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/NodeTracker.java?rev=1575755&view=auto ============================================================================== --- commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/NodeTracker.java (added) +++ commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/NodeTracker.java Sun Mar 9 20:55:21 2014 @@ -0,0 +1,330 @@ +/* + * 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 java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.configuration.ex.ConfigurationRuntimeException; + +/** + * <p> + * A class which can track specific nodes in an {@link InMemoryNodeModel}. + * </p> + * <p> + * Sometimes it is necessary to keep track on a specific node, for instance when + * operating on a subtree of a model. For a model comprised of immutable nodes + * this is not trivial because each update of the model may cause the node to be + * replaced. So holding a direct pointer onto the target node is not an option; + * this instance may become outdated. + * </p> + * <p> + * This class provides an API for selecting a specific node by using a + * {@link NodeSelector}. The selector is used to obtain an initial reference to + * the target node. It is then applied again after each update of the associated + * node model (which is done in the {@code update()} method). At this point of + * time two things can happen: + * <ul> + * <li>The {@code NodeSelector} associated with the tracked node still selects a + * single node. Then this node becomes the new tracked node. This may be the + * same instance as before or a new one.</li> + * <li>The selector does no longer find the target node. This can happen for + * instance if it has been removed by an operation. In this case, the previous + * node instance is used. It is now detached from the model, but can still be + * used for operations on this subtree. It may even become life again after + * another update of the model.</li> + * </ul> + * </p> + * <p> + * Implementation note: This class is intended to work in a concurrent + * environment. Instances are immutable. The represented state can be updated by + * creating new instances which are then stored by the owning node model. + * </p> + * + * @version $Id$ + * @since 2.0 + */ +class NodeTracker +{ + /** A map with data about tracked nodes. */ + private final Map<NodeSelector, TrackedNodeData> trackedNodes; + + /** + * Creates a new instance of {@code NodeTracker}. This instance does not yet + * track any nodes. + */ + public NodeTracker() + { + this(Collections.<NodeSelector, TrackedNodeData> emptyMap()); + } + + /** + * Creates a new instance of {@code NodeTracker} and initializes it with the + * given map of tracked nodes. This constructor is used internally when the + * state of tracked nodes has changed. + * + * @param map the map with tracked nodes + */ + private NodeTracker(Map<NodeSelector, TrackedNodeData> map) + { + trackedNodes = map; + } + + /** + * Adds a node to be tracked. The passed in selector must select exactly one + * target node, otherwise an exception is thrown. A new instance is created + * with the updated tracking state. + * + * @param root the root node + * @param selector the {@code NodeSelector} + * @param resolver the {@code NodeKeyResolver} + * @param handler the {@code NodeHandler} + * @return the updated instance + * @throws ConfigurationRuntimeException if the selector does not select a + * single node + */ + public NodeTracker trackNode(ImmutableNode root, NodeSelector selector, + NodeKeyResolver<ImmutableNode> resolver, + NodeHandler<ImmutableNode> handler) + { + Map<NodeSelector, TrackedNodeData> newState = + new HashMap<NodeSelector, TrackedNodeData>(trackedNodes); + TrackedNodeData trackData = newState.get(selector); + newState.put( + selector, + trackDataForAddedObserver(root, selector, resolver, handler, + trackData)); + return new NodeTracker(newState); + } + + /** + * Notifies this object that an observer was removed for the specified + * tracked node. If this was the last observer, the track data for this + * selector can be removed. + * + * @param selector the {@code NodeSelector} + * @return the updated instance + * @throws ConfigurationRuntimeException if no information about this node + * is available + */ + public NodeTracker untrackNode(NodeSelector selector) + { + TrackedNodeData trackData = getTrackedNodeData(selector); + + Map<NodeSelector, TrackedNodeData> newState = + new HashMap<NodeSelector, TrackedNodeData>(trackedNodes); + TrackedNodeData newTrackData = trackData.observerRemoved(); + if (newTrackData == null) + { + newState.remove(selector); + } + else + { + newState.put(selector, newTrackData); + } + return new NodeTracker(newState); + } + + /** + * Returns the current {@code ImmutableNode} instance associated with the + * given selector. + * + * @param selector the {@code NodeSelector} + * @return the {@code ImmutableNode} selected by this selector + * @throws ConfigurationRuntimeException if no data for this selector is + * available + */ + public ImmutableNode getTrackedNode(NodeSelector selector) + { + return getTrackedNodeData(selector).getNode(); + } + + /** + * Updates tracking information after the node structure has been changed. + * This method iterates over all tracked nodes. The selectors are evaluated + * again to update the node reference. If this fails for a selector, the + * previous node is reused; this tracked node is then detached. + * + * @param root the root node + * @param resolver the {@code NodeKeyResolver} + * @param handler the {@code NodeHandler} + * @return the updated instance + */ + public NodeTracker update(ImmutableNode root, + NodeKeyResolver<ImmutableNode> resolver, + NodeHandler<ImmutableNode> handler) + { + if (trackedNodes.isEmpty()) + { + // there is not state to be updated + return this; + } + + Map<NodeSelector, TrackedNodeData> newState = + new HashMap<NodeSelector, TrackedNodeData>(); + for (Map.Entry<NodeSelector, TrackedNodeData> e : trackedNodes + .entrySet()) + { + ImmutableNode newTarget = + e.getKey().select(root, resolver, handler); + TrackedNodeData newTrackData = + (newTarget != null) ? e.getValue().updateNode(newTarget) + : e.getValue(); + newState.put(e.getKey(), newTrackData); + } + + return new NodeTracker(newState); + } + + /** + * Obtains the {@code TrackedNodeData} object for the specified selector. If + * the selector cannot be resolved, an exception is thrown. + * + * @param selector the {@code NodeSelector} + * @return the {@code TrackedNodeData} object for this selector + * @throws ConfigurationRuntimeException if the selector cannot be resolved + */ + private TrackedNodeData getTrackedNodeData(NodeSelector selector) + { + TrackedNodeData trackData = trackedNodes.get(selector); + if (trackData == null) + { + throw new ConfigurationRuntimeException("No tracked node found: " + + selector); + } + return trackData; + } + + /** + * Creates a {@code TrackedNodeData} object for a newly added observer for + * the specified node selector. + * + * @param root the root node + * @param selector the {@code NodeSelector} + * @param resolver the {@code NodeKeyResolver} + * @param handler the {@code NodeHandler} + * @param trackData the current data for this selector + * @return the updated {@code TrackedNodeData} + * @throws ConfigurationRuntimeException if the selector does not select a + * single node + */ + private static TrackedNodeData trackDataForAddedObserver( + ImmutableNode root, NodeSelector selector, + NodeKeyResolver<ImmutableNode> resolver, + NodeHandler<ImmutableNode> handler, TrackedNodeData trackData) + { + if (trackData != null) + { + return trackData.observerAdded(); + } + else + { + ImmutableNode target = selector.select(root, resolver, handler); + if (target == null) + { + throw new ConfigurationRuntimeException( + "Selector does not select unique node: " + selector); + } + return new TrackedNodeData(target); + } + } + + /** + * A simple data class holding information about a tracked node. + */ + private static class TrackedNodeData + { + /** The current instance of the tracked node. */ + private final ImmutableNode node; + + /** The number of observers of this tracked node. */ + private final int observerCount; + + /** + * Creates a new instance of {@code TrackedNodeData} and initializes it + * with the current reference to the tracked node. + * + * @param nd the tracked node + */ + public TrackedNodeData(ImmutableNode nd) + { + this(nd, 1); + } + + /** + * Creates a new instance of {@code TrackedNodeData} and initializes its + * properties. + * + * @param nd the tracked node + * @param obsCount the observer count + */ + private TrackedNodeData(ImmutableNode nd, int obsCount) + { + node = nd; + observerCount = obsCount; + } + + /** + * Returns the tracked node. + * + * @return the tracked node + */ + public ImmutableNode getNode() + { + return node; + } + + /** + * Another observer was added for this tracked node. This method returns + * a new instance with an adjusted observer count. + * + * @return the updated instance + */ + public TrackedNodeData observerAdded() + { + return new TrackedNodeData(node, observerCount + 1); + } + + /** + * An observer for this tracked node was removed. This method returns a + * new instance with an adjusted observer count. If there are no more + * observers, result is <b>null</b>. This means that this node is no + * longer tracked and can be released. + * + * @return the updated instance or <b>null</b> + */ + public TrackedNodeData observerRemoved() + { + return (observerCount <= 1) ? null : new TrackedNodeData(node, + observerCount - 1); + } + + /** + * Updates the node reference. This method is called after an update of + * the underlying node structure if the tracked node was replaced by + * another instance. + * + * @param newNode the new tracked node instance + * @return the updated instance + */ + public TrackedNodeData updateNode(ImmutableNode newNode) + { + return new TrackedNodeData(newNode, observerCount); + } + } +}