/*
 * 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.configuration2.tree;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;

A class which can track specific nodes in an InMemoryNodeModel.

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.

This class provides an API for selecting a specific node by using a 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 update() method). At this point of time two things can happen:

  • The 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.
  • 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.

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.

Since:2.0
/** * <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> * * @since 2.0 */
class NodeTracker {
A map with data about tracked nodes.
/** A map with data about tracked nodes. */
private final Map<NodeSelector, TrackedNodeData> trackedNodes;
Creates a new instance of NodeTracker. This instance does not yet track any nodes.
/** * 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 NodeTracker and initializes it with the given map of tracked nodes. This constructor is used internally when the state of tracked nodes has changed.
Params:
  • map – the map with tracked nodes
/** * 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(final 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.
Params:
  • root – the root node
  • selector – the NodeSelector
  • resolver – the NodeKeyResolver
  • handler – the NodeHandler
Throws:
Returns:the updated instance
/** * 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(final ImmutableNode root, final NodeSelector selector, final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler) { final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes); final TrackedNodeData trackData = newState.get(selector); newState.put( selector, trackDataForAddedObserver(root, selector, resolver, handler, trackData)); return new NodeTracker(newState); }
Adds a number of nodes to be tracked. For each node in the passed in collection, a tracked node entry is created unless already one exists.
Params:
  • selectors – a collection with the NodeSelector objects
  • nodes – a collection with the nodes to be tracked
Returns:the updated instance
/** * Adds a number of nodes to be tracked. For each node in the passed in * collection, a tracked node entry is created unless already one exists. * * @param selectors a collection with the {@code NodeSelector} objects * @param nodes a collection with the nodes to be tracked * @return the updated instance */
public NodeTracker trackNodes(final Collection<NodeSelector> selectors, final Collection<ImmutableNode> nodes) { final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes); final Iterator<ImmutableNode> itNodes = nodes.iterator(); for (final NodeSelector selector : selectors) { final ImmutableNode node = itNodes.next(); TrackedNodeData trackData = newState.get(selector); if (trackData == null) { trackData = new TrackedNodeData(node); } else { trackData = trackData.observerAdded(); } newState.put(selector, 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.
Params:
  • selector – the NodeSelector
Throws:
Returns:the updated instance
/** * 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(final NodeSelector selector) { final TrackedNodeData trackData = getTrackedNodeData(selector); final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes); final TrackedNodeData newTrackData = trackData.observerRemoved(); if (newTrackData == null) { newState.remove(selector); } else { newState.put(selector, newTrackData); } return new NodeTracker(newState); }
Returns the current ImmutableNode instance associated with the given selector.
Params:
  • selector – the NodeSelector
Throws:
Returns:the ImmutableNode selected by this selector
/** * 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(final NodeSelector selector) { return getTrackedNodeData(selector).getNode(); }
Returns a flag whether the specified tracked node is detached.
Params:
  • selector – the NodeSelector
Throws:
Returns:a flag whether this node is detached
/** * Returns a flag whether the specified tracked node is detached. * * @param selector the {@code NodeSelector} * @return a flag whether this node is detached * @throws ConfigurationRuntimeException if no data for this selector is * available */
public boolean isTrackedNodeDetached(final NodeSelector selector) { return getTrackedNodeData(selector).isDetached(); }
Returns the detached node model for the specified tracked node. When a node becomes detached, operations on it are independent from the original model. To implement this, a separate node model is created wrapping this tracked node. This model can be queried by this method. If the node affected is not detached, result is null.
Params:
  • selector – the NodeSelector
Throws:
Returns:the detached node model for this node or null
/** * Returns the detached node model for the specified tracked node. When a * node becomes detached, operations on it are independent from the original * model. To implement this, a separate node model is created wrapping this * tracked node. This model can be queried by this method. If the node * affected is not detached, result is <b>null</b>. * * @param selector the {@code NodeSelector} * @return the detached node model for this node or <b>null</b> * @throws ConfigurationRuntimeException if no data for this selector is * available */
public InMemoryNodeModel getDetachedNodeModel(final NodeSelector selector) { return getTrackedNodeData(selector).getDetachedModel(); }
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. The passed in NodeSelector is the selector of the tracked node which is the target of the current transaction. (It is null if the transaction is not executed on a tracked node.) This is used to handle a special case: if the tracked node becomes detached by an operation targeting itself, this means that the node has been cleared by this operation. In this case, the previous node instance is not used, but an empty node is created.
Params:
  • root – the root node
  • txTarget – the NodeSelector referencing the target node of the current transaction (may be null)
  • resolver – the NodeKeyResolver
  • handler – the NodeHandler
Returns:the updated instance
/** * 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. The passed * in {@code NodeSelector} is the selector of the tracked node which is the * target of the current transaction. (It is <b>null</b> if the transaction * is not executed on a tracked node.) This is used to handle a special * case: if the tracked node becomes detached by an operation targeting * itself, this means that the node has been cleared by this operation. In * this case, the previous node instance is not used, but an empty node is * created. * * @param root the root node * @param txTarget the {@code NodeSelector} referencing the target node of * the current transaction (may be <b>null</b>) * @param resolver the {@code NodeKeyResolver} * @param handler the {@code NodeHandler} * @return the updated instance */
public NodeTracker update(final ImmutableNode root, final NodeSelector txTarget, final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler) { if (trackedNodes.isEmpty()) { // there is not state to be updated return this; } final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(); for (final Map.Entry<NodeSelector, TrackedNodeData> e : trackedNodes .entrySet()) { newState.put( e.getKey(), determineUpdatedTrackedNodeData(root, txTarget, resolver, handler, e)); } return new NodeTracker(newState); }
Marks all tracked nodes as detached. This method is called if there are some drastic changes on the underlying node structure, e.g. if the root node was replaced.
Returns:the updated instance
/** * Marks all tracked nodes as detached. This method is called if there are * some drastic changes on the underlying node structure, e.g. if the root * node was replaced. * * @return the updated instance */
public NodeTracker detachAllTrackedNodes() { if (trackedNodes.isEmpty()) { // there is not state to be updated return this; } final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(); for (final Map.Entry<NodeSelector, TrackedNodeData> e : trackedNodes .entrySet()) { final TrackedNodeData newData = e.getValue().isDetached() ? e.getValue() : e.getValue() .detach(null); newState.put(e.getKey(), newData); } return new NodeTracker(newState); }
Replaces a tracked node by another one. This operation causes the tracked node to become detached.
Params:
  • selector – the NodeSelector
  • newNode – the replacement node
Throws:
Returns:the updated instance
/** * Replaces a tracked node by another one. This operation causes the tracked * node to become detached. * * @param selector the {@code NodeSelector} * @param newNode the replacement node * @return the updated instance * @throws ConfigurationRuntimeException if the selector cannot be resolved */
public NodeTracker replaceAndDetachTrackedNode(final NodeSelector selector, final ImmutableNode newNode) { final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes); newState.put(selector, getTrackedNodeData(selector).detach(newNode)); return new NodeTracker(newState); }
Obtains the TrackedNodeData object for the specified selector. If the selector cannot be resolved, an exception is thrown.
Params:
  • selector – the NodeSelector
Throws:
Returns:the TrackedNodeData object for this selector
/** * 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(final NodeSelector selector) { final TrackedNodeData trackData = trackedNodes.get(selector); if (trackData == null) { throw new ConfigurationRuntimeException("No tracked node found: " + selector); } return trackData; }
Returns a TrackedNodeData object for an update operation. If the tracked node is still life, its selector is applied to the current root node. It may become detached if there is no match.
Params:
  • root – the root node
  • txTarget – the NodeSelector referencing the target node of the current transaction (may be null)
  • resolver – the NodeKeyResolver
  • handler – the NodeHandler
  • e – the current selector and TrackedNodeData
Returns:the updated TrackedNodeData
/** * Returns a {@code TrackedNodeData} object for an update operation. If the * tracked node is still life, its selector is applied to the current root * node. It may become detached if there is no match. * * @param root the root node * @param txTarget the {@code NodeSelector} referencing the target node of * the current transaction (may be <b>null</b>) * @param resolver the {@code NodeKeyResolver} * @param handler the {@code NodeHandler} * @param e the current selector and {@code TrackedNodeData} * @return the updated {@code TrackedNodeData} */
private static TrackedNodeData determineUpdatedTrackedNodeData( final ImmutableNode root, final NodeSelector txTarget, final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler, final Map.Entry<NodeSelector, TrackedNodeData> e) { if (e.getValue().isDetached()) { return e.getValue(); } ImmutableNode newTarget; try { newTarget = e.getKey().select(root, resolver, handler); } catch (final Exception ex) { /* Evaluation of the key caused an exception. This can happen for instance if the expression engine was changed. In this case, the node becomes detached. */ newTarget = null; } if (newTarget == null) { return detachedTrackedNodeData(txTarget, e); } return e.getValue().updateNode(newTarget); }
Creates a new TrackedNodeData object for a tracked node which becomes detached within the current transaction. This method checks whether the affected node is the root node of the current transaction. If so, it is cleared.
Params:
  • txTarget – the NodeSelector referencing the target node of the current transaction (may be null)
  • e – the current selector and TrackedNodeData
Returns:the new TrackedNodeData object to be used for this tracked node
/** * Creates a new {@code TrackedNodeData} object for a tracked node which * becomes detached within the current transaction. This method checks * whether the affected node is the root node of the current transaction. If * so, it is cleared. * * @param txTarget the {@code NodeSelector} referencing the target node of * the current transaction (may be <b>null</b>) * @param e the current selector and {@code TrackedNodeData} * @return the new {@code TrackedNodeData} object to be used for this * tracked node */
private static TrackedNodeData detachedTrackedNodeData( final NodeSelector txTarget, final Map.Entry<NodeSelector, TrackedNodeData> e) { final ImmutableNode newNode = e.getKey().equals(txTarget) ? createEmptyTrackedNode(e .getValue()) : null; return e.getValue().detach(newNode); }
Creates an empty node derived from the passed in TrackedNodeData object. This method is called if a tracked node got cleared by a transaction.
Params:
  • data – the TrackedNodeData
Returns:the new node instance for this tracked node
/** * Creates an empty node derived from the passed in {@code TrackedNodeData} * object. This method is called if a tracked node got cleared by a * transaction. * * @param data the {@code TrackedNodeData} * @return the new node instance for this tracked node */
private static ImmutableNode createEmptyTrackedNode(final TrackedNodeData data) { return new ImmutableNode.Builder().name(data.getNode().getNodeName()) .create(); }
Creates a TrackedNodeData object for a newly added observer for the specified node selector.
Params:
  • root – the root node
  • selector – the NodeSelector
  • resolver – the NodeKeyResolver
  • handler – the NodeHandler
  • trackData – the current data for this selector
Throws:
Returns:the updated TrackedNodeData
/** * 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( final ImmutableNode root, final NodeSelector selector, final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler, final TrackedNodeData trackData) { if (trackData != null) { return trackData.observerAdded(); } final 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.
/** * A simple data class holding information about a tracked node. */
private static class TrackedNodeData {
The current instance of the tracked node.
/** The current instance of the tracked node. */
private final ImmutableNode node;
The number of observers of this tracked node.
/** The number of observers of this tracked node. */
private final int observerCount;
A node model to be used when the tracked node is detached.
/** A node model to be used when the tracked node is detached. */
private final InMemoryNodeModel detachedModel;
Creates a new instance of TrackedNodeData and initializes it with the current reference to the tracked node.
Params:
  • nd – the tracked node
/** * 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(final ImmutableNode nd) { this(nd, 1, null); }
Creates a new instance of TrackedNodeData and initializes its properties.
Params:
  • nd – the tracked node
  • obsCount – the observer count
  • detachedNodeModel – a model to be used in detached mode
/** * Creates a new instance of {@code TrackedNodeData} and initializes its * properties. * * @param nd the tracked node * @param obsCount the observer count * @param detachedNodeModel a model to be used in detached mode */
private TrackedNodeData(final ImmutableNode nd, final int obsCount, final InMemoryNodeModel detachedNodeModel) { node = nd; observerCount = obsCount; detachedModel = detachedNodeModel; }
Returns the tracked node.
Returns:the tracked node
/** * Returns the tracked node. * * @return the tracked node */
public ImmutableNode getNode() { return getDetachedModel() != null ? getDetachedModel() .getRootNode() : node; }
Returns the node model to be used in detached mode. This is null if the represented tracked node is not detached.
Returns:the node model in detached mode
/** * Returns the node model to be used in detached mode. This is * <b>null</b> if the represented tracked node is not detached. * * @return the node model in detached mode */
public InMemoryNodeModel getDetachedModel() { return detachedModel; }
Returns a flag whether the represented tracked node is detached.
Returns:the detached flag
/** * Returns a flag whether the represented tracked node is detached. * * @return the detached flag */
public boolean isDetached() { return getDetachedModel() != null; }
Another observer was added for this tracked node. This method returns a new instance with an adjusted observer count.
Returns:the updated instance
/** * 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, getDetachedModel()); }
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 null. This means that this node is no longer tracked and can be released.
Returns:the updated instance or null
/** * 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, getDetachedModel()); }
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.
Params:
  • newNode – the new tracked node instance
Returns:the updated instance
/** * 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(final ImmutableNode newNode) { return new TrackedNodeData(newNode, observerCount, getDetachedModel()); }
Returns an instance with the detached flag set to true. This method is called if the selector of a tracked node does not match a single node any more. It is possible to pass in a new node instance which becomes the current tracked node. If this is null, the previous node instance is used.
Params:
  • newNode – the new tracked node instance (may be null)
Returns:the updated instance
/** * Returns an instance with the detached flag set to true. This method * is called if the selector of a tracked node does not match a single * node any more. It is possible to pass in a new node instance which * becomes the current tracked node. If this is <b>null</b>, the * previous node instance is used. * * @param newNode the new tracked node instance (may be <b>null</b>) * @return the updated instance */
public TrackedNodeData detach(final ImmutableNode newNode) { final ImmutableNode newTrackedNode = newNode != null ? newNode : getNode(); return new TrackedNodeData(newTrackedNode, observerCount, new InMemoryNodeModel(newTrackedNode)); } } }