All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.configuration2.tree.NodeTracker Maven / Gradle / Ivy

Go to download

Tools to assist in the reading of configuration/preferences files in various formats

There is a newer version: 2.10.1
Show newest version
/*
 * 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 {@link 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 * {@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: *

    *
  • 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.
  • *
  • 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 */ class NodeTracker { /** A map with data about tracked nodes. */ private final Map trackedNodes; /** * Creates a new instance of {@code NodeTracker}. This instance does not yet * track any nodes. */ public NodeTracker() { this(Collections. 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 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 resolver, final NodeHandler handler) { final Map 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 selectors, final Collection nodes) { final Map newState = new HashMap<>(trackedNodes); final Iterator 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 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 null. * * @param selector the {@code NodeSelector} * @return the detached node model for this node or null * @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 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. * * @param root the root node * @param txTarget the {@code NodeSelector} referencing the target node of * the current transaction (may be null) * @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 resolver, final NodeHandler handler) { if (trackedNodes.isEmpty()) { // there is not state to be updated return this; } final Map newState = new HashMap<>(); for (final Map.Entry 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 newState = new HashMap<>(); for (final Map.Entry 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 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 null) * @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 resolver, final NodeHandler handler, final Map.Entry 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 null) * @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 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 resolver, final NodeHandler 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 * null 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 null. This means that this node is no * longer tracked and can be released. * * @return the updated instance or null */ 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 null, the * previous node instance is used. * * @param newNode the new tracked node instance (may be null) * @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)); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy