blob: afbffefa98e055698ca19ff31fc1ac901d9b2303 [file] [log] [blame]
/*
* 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;
/**
* <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. */
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(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.
*
* @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.
*
* @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.
*
* @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 {@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.
*
* @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 <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 {@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.
*
* @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.
*
* @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 {@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 {@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 {@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 {@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 {@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.
*/
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;
/** A node model to be used when the tracked node is detached. */
private final InMemoryNodeModel detachedModel;
/**
* 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 {@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.
*
* @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
* <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.
*
* @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.
*
* @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 <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.
*
* @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 <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));
}
}
}