| /* |
| * 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 |
| * |
| * https://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 java.util.stream.Collectors; |
| |
| 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 |
| */ |
| final class NodeTracker { |
| |
| /** |
| * A simple data class holding information about a tracked node. |
| */ |
| private static final 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 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 <strong>null</strong>, the previous node instance is used. |
| * |
| * @param newNode the new tracked node instance (may be <strong>null</strong>) |
| * @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)); |
| } |
| |
| /** |
| * Gets the node model to be used in detached mode. This is <strong>null</strong> if the represented tracked node is not |
| * detached. |
| * |
| * @return the node model in detached mode |
| */ |
| public InMemoryNodeModel getDetachedModel() { |
| return detachedModel; |
| } |
| |
| /** |
| * Gets the tracked node. |
| * |
| * @return the tracked node |
| */ |
| public ImmutableNode getNode() { |
| return getDetachedModel() != null ? getDetachedModel().getRootNode() : node; |
| } |
| |
| /** |
| * 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 <strong>null</strong>. This means that this node is no longer tracked and can be |
| * released. |
| * |
| * @return the updated instance or <strong>null</strong> |
| */ |
| 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()); |
| } |
| } |
| |
| /** |
| * 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 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 <strong>null</strong>) |
| * @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); |
| } |
| |
| /** |
| * 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 <strong>null</strong>) |
| * @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 {@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 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; |
| } |
| |
| /** |
| * Marks all tracked nodes as detached. This method is called if there are some drastic changes on the underlying node |
| * structure, for example 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; |
| } |
| return new NodeTracker(trackedNodes.entrySet().stream() |
| .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().isDetached() ? e.getValue() : e.getValue().detach(null)))); |
| } |
| |
| /** |
| * Gets 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 <strong>null</strong>. |
| * |
| * @param selector the {@code NodeSelector} |
| * @return the detached node model for this node or <strong>null</strong> |
| * @throws ConfigurationRuntimeException if no data for this selector is available |
| */ |
| public InMemoryNodeModel getDetachedNodeModel(final NodeSelector selector) { |
| return getTrackedNodeData(selector).getDetachedModel(); |
| } |
| |
| /** |
| * Gets 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(); |
| } |
| |
| /** |
| * 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 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(); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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(); |
| selectors.forEach(selector -> { |
| 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); |
| } |
| |
| /** |
| * 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 <strong>null</strong> 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 <strong>null</strong>) |
| * @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<>(); |
| trackedNodes.entrySet().forEach(e -> newState.put(e.getKey(), determineUpdatedTrackedNodeData(root, txTarget, resolver, handler, e))); |
| return new NodeTracker(newState); |
| } |
| } |