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

The 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 java.util.stream.Collectors;

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 */ 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 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)); } /** * Gets 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; } /** * 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 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()); } } /** * 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 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); } /** * 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 {@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 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; } /** * 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; } 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 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(); } /** * 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 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 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(); 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 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 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<>(); trackedNodes.entrySet().forEach(e -> newState.put(e.getKey(), determineUpdatedTrackedNodeData(root, txTarget, resolver, handler, e))); return new NodeTracker(newState); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy