NodeTracker.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.configuration2.tree;

  18. import java.util.Collection;
  19. import java.util.Collections;
  20. import java.util.HashMap;
  21. import java.util.Iterator;
  22. import java.util.Map;
  23. import java.util.stream.Collectors;

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

  25. /**
  26.  * <p>
  27.  * A class which can track specific nodes in an {@link InMemoryNodeModel}.
  28.  * </p>
  29.  * <p>
  30.  * Sometimes it is necessary to keep track on a specific node, for instance when operating on a subtree of a model. For
  31.  * a model comprised of immutable nodes this is not trivial because each update of the model may cause the node to be
  32.  * replaced. So holding a direct pointer onto the target node is not an option; this instance may become outdated.
  33.  * </p>
  34.  * <p>
  35.  * This class provides an API for selecting a specific node by using a {@link NodeSelector}. The selector is used to
  36.  * obtain an initial reference to the target node. It is then applied again after each update of the associated node
  37.  * model (which is done in the {@code update()} method). At this point of time two things can happen:
  38.  * <ul>
  39.  * <li>The {@code NodeSelector} associated with the tracked node still selects a single node. Then this node becomes the
  40.  * new tracked node. This may be the same instance as before or a new one.</li>
  41.  * <li>The selector does no longer find the target node. This can happen for instance if it has been removed by an
  42.  * operation. In this case, the previous node instance is used. It is now detached from the model, but can still be used
  43.  * for operations on this subtree. It may even become life again after another update of the model.</li>
  44.  * </ul>
  45.  * </p>
  46.  * <p>
  47.  * Implementation note: This class is intended to work in a concurrent environment. Instances are immutable. The
  48.  * represented state can be updated by creating new instances which are then stored by the owning node model.
  49.  * </p>
  50.  *
  51.  * @since 2.0
  52.  */
  53. final class NodeTracker {
  54.     /**
  55.      * A simple data class holding information about a tracked node.
  56.      */
  57.     private static final class TrackedNodeData {
  58.         /** The current instance of the tracked node. */
  59.         private final ImmutableNode node;

  60.         /** The number of observers of this tracked node. */
  61.         private final int observerCount;

  62.         /** A node model to be used when the tracked node is detached. */
  63.         private final InMemoryNodeModel detachedModel;

  64.         /**
  65.          * Creates a new instance of {@code TrackedNodeData} and initializes it with the current reference to the tracked node.
  66.          *
  67.          * @param nd the tracked node
  68.          */
  69.         public TrackedNodeData(final ImmutableNode nd) {
  70.             this(nd, 1, null);
  71.         }

  72.         /**
  73.          * Creates a new instance of {@code TrackedNodeData} and initializes its properties.
  74.          *
  75.          * @param nd the tracked node
  76.          * @param obsCount the observer count
  77.          * @param detachedNodeModel a model to be used in detached mode
  78.          */
  79.         private TrackedNodeData(final ImmutableNode nd, final int obsCount, final InMemoryNodeModel detachedNodeModel) {
  80.             node = nd;
  81.             observerCount = obsCount;
  82.             detachedModel = detachedNodeModel;
  83.         }

  84.         /**
  85.          * Returns an instance with the detached flag set to true. This method is called if the selector of a tracked node does
  86.          * not match a single node any more. It is possible to pass in a new node instance which becomes the current tracked
  87.          * node. If this is <strong>null</strong>, the previous node instance is used.
  88.          *
  89.          * @param newNode the new tracked node instance (may be <strong>null</strong>)
  90.          * @return the updated instance
  91.          */
  92.         public TrackedNodeData detach(final ImmutableNode newNode) {
  93.             final ImmutableNode newTrackedNode = newNode != null ? newNode : getNode();
  94.             return new TrackedNodeData(newTrackedNode, observerCount, new InMemoryNodeModel(newTrackedNode));
  95.         }

  96.         /**
  97.          * Gets the node model to be used in detached mode. This is <strong>null</strong> if the represented tracked node is not
  98.          * detached.
  99.          *
  100.          * @return the node model in detached mode
  101.          */
  102.         public InMemoryNodeModel getDetachedModel() {
  103.             return detachedModel;
  104.         }

  105.         /**
  106.          * Gets the tracked node.
  107.          *
  108.          * @return the tracked node
  109.          */
  110.         public ImmutableNode getNode() {
  111.             return getDetachedModel() != null ? getDetachedModel().getRootNode() : node;
  112.         }

  113.         /**
  114.          * Returns a flag whether the represented tracked node is detached.
  115.          *
  116.          * @return the detached flag
  117.          */
  118.         public boolean isDetached() {
  119.             return getDetachedModel() != null;
  120.         }

  121.         /**
  122.          * Another observer was added for this tracked node. This method returns a new instance with an adjusted observer count.
  123.          *
  124.          * @return the updated instance
  125.          */
  126.         public TrackedNodeData observerAdded() {
  127.             return new TrackedNodeData(node, observerCount + 1, getDetachedModel());
  128.         }

  129.         /**
  130.          * An observer for this tracked node was removed. This method returns a new instance with an adjusted observer count. If
  131.          * there are no more observers, result is <strong>null</strong>. This means that this node is no longer tracked and can be
  132.          * released.
  133.          *
  134.          * @return the updated instance or <strong>null</strong>
  135.          */
  136.         public TrackedNodeData observerRemoved() {
  137.             return observerCount <= 1 ? null : new TrackedNodeData(node, observerCount - 1, getDetachedModel());
  138.         }

  139.         /**
  140.          * Updates the node reference. This method is called after an update of the underlying node structure if the tracked
  141.          * node was replaced by another instance.
  142.          *
  143.          * @param newNode the new tracked node instance
  144.          * @return the updated instance
  145.          */
  146.         public TrackedNodeData updateNode(final ImmutableNode newNode) {
  147.             return new TrackedNodeData(newNode, observerCount, getDetachedModel());
  148.         }
  149.     }

  150.     /**
  151.      * Creates an empty node derived from the passed in {@code TrackedNodeData} object. This method is called if a tracked
  152.      * node got cleared by a transaction.
  153.      *
  154.      * @param data the {@code TrackedNodeData}
  155.      * @return the new node instance for this tracked node
  156.      */
  157.     private static ImmutableNode createEmptyTrackedNode(final TrackedNodeData data) {
  158.         return new ImmutableNode.Builder().name(data.getNode().getNodeName()).create();
  159.     }

  160.     /**
  161.      * Creates a new {@code TrackedNodeData} object for a tracked node which becomes detached within the current
  162.      * transaction. This method checks whether the affected node is the root node of the current transaction. If so, it is
  163.      * cleared.
  164.      *
  165.      * @param txTarget the {@code NodeSelector} referencing the target node of the current transaction (may be <strong>null</strong>)
  166.      * @param e the current selector and {@code TrackedNodeData}
  167.      * @return the new {@code TrackedNodeData} object to be used for this tracked node
  168.      */
  169.     private static TrackedNodeData detachedTrackedNodeData(final NodeSelector txTarget, final Map.Entry<NodeSelector, TrackedNodeData> e) {
  170.         final ImmutableNode newNode = e.getKey().equals(txTarget) ? createEmptyTrackedNode(e.getValue()) : null;
  171.         return e.getValue().detach(newNode);
  172.     }

  173.     /**
  174.      * Returns a {@code TrackedNodeData} object for an update operation. If the tracked node is still life, its selector is
  175.      * applied to the current root node. It may become detached if there is no match.
  176.      *
  177.      * @param root the root node
  178.      * @param txTarget the {@code NodeSelector} referencing the target node of the current transaction (may be <strong>null</strong>)
  179.      * @param resolver the {@code NodeKeyResolver}
  180.      * @param handler the {@code NodeHandler}
  181.      * @param e the current selector and {@code TrackedNodeData}
  182.      * @return the updated {@code TrackedNodeData}
  183.      */
  184.     private static TrackedNodeData determineUpdatedTrackedNodeData(final ImmutableNode root, final NodeSelector txTarget,
  185.         final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler, final Map.Entry<NodeSelector, TrackedNodeData> e) {
  186.         if (e.getValue().isDetached()) {
  187.             return e.getValue();
  188.         }

  189.         ImmutableNode newTarget;
  190.         try {
  191.             newTarget = e.getKey().select(root, resolver, handler);
  192.         } catch (final Exception ex) {
  193.             /*
  194.              * Evaluation of the key caused an exception. This can happen for instance if the expression engine was changed. In this
  195.              * case, the node becomes detached.
  196.              */
  197.             newTarget = null;
  198.         }
  199.         if (newTarget == null) {
  200.             return detachedTrackedNodeData(txTarget, e);
  201.         }
  202.         return e.getValue().updateNode(newTarget);
  203.     }

  204.     /**
  205.      * Creates a {@code TrackedNodeData} object for a newly added observer for the specified node selector.
  206.      *
  207.      * @param root the root node
  208.      * @param selector the {@code NodeSelector}
  209.      * @param resolver the {@code NodeKeyResolver}
  210.      * @param handler the {@code NodeHandler}
  211.      * @param trackData the current data for this selector
  212.      * @return the updated {@code TrackedNodeData}
  213.      * @throws ConfigurationRuntimeException if the selector does not select a single node
  214.      */
  215.     private static TrackedNodeData trackDataForAddedObserver(final ImmutableNode root, final NodeSelector selector,
  216.         final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler, final TrackedNodeData trackData) {
  217.         if (trackData != null) {
  218.             return trackData.observerAdded();
  219.         }
  220.         final ImmutableNode target = selector.select(root, resolver, handler);
  221.         if (target == null) {
  222.             throw new ConfigurationRuntimeException("Selector does not select unique node: " + selector);
  223.         }
  224.         return new TrackedNodeData(target);
  225.     }

  226.     /** A map with data about tracked nodes. */
  227.     private final Map<NodeSelector, TrackedNodeData> trackedNodes;

  228.     /**
  229.      * Creates a new instance of {@code NodeTracker}. This instance does not yet track any nodes.
  230.      */
  231.     public NodeTracker() {
  232.         this(Collections.<NodeSelector, TrackedNodeData>emptyMap());
  233.     }

  234.     /**
  235.      * Creates a new instance of {@code NodeTracker} and initializes it with the given map of tracked nodes. This
  236.      * constructor is used internally when the state of tracked nodes has changed.
  237.      *
  238.      * @param map the map with tracked nodes
  239.      */
  240.     private NodeTracker(final Map<NodeSelector, TrackedNodeData> map) {
  241.         trackedNodes = map;
  242.     }

  243.     /**
  244.      * Marks all tracked nodes as detached. This method is called if there are some drastic changes on the underlying node
  245.      * structure, for example if the root node was replaced.
  246.      *
  247.      * @return the updated instance
  248.      */
  249.     public NodeTracker detachAllTrackedNodes() {
  250.         if (trackedNodes.isEmpty()) {
  251.             // there is not state to be updated
  252.             return this;
  253.         }
  254.         return new NodeTracker(trackedNodes.entrySet().stream()
  255.             .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().isDetached() ? e.getValue() : e.getValue().detach(null))));
  256.     }

  257.     /**
  258.      * Gets the detached node model for the specified tracked node. When a node becomes detached, operations on it are
  259.      * independent from the original model. To implement this, a separate node model is created wrapping this tracked node.
  260.      * This model can be queried by this method. If the node affected is not detached, result is <strong>null</strong>.
  261.      *
  262.      * @param selector the {@code NodeSelector}
  263.      * @return the detached node model for this node or <strong>null</strong>
  264.      * @throws ConfigurationRuntimeException if no data for this selector is available
  265.      */
  266.     public InMemoryNodeModel getDetachedNodeModel(final NodeSelector selector) {
  267.         return getTrackedNodeData(selector).getDetachedModel();
  268.     }

  269.     /**
  270.      * Gets the current {@code ImmutableNode} instance associated with the given selector.
  271.      *
  272.      * @param selector the {@code NodeSelector}
  273.      * @return the {@code ImmutableNode} selected by this selector
  274.      * @throws ConfigurationRuntimeException if no data for this selector is available
  275.      */
  276.     public ImmutableNode getTrackedNode(final NodeSelector selector) {
  277.         return getTrackedNodeData(selector).getNode();
  278.     }

  279.     /**
  280.      * Obtains the {@code TrackedNodeData} object for the specified selector. If the selector cannot be resolved, an
  281.      * exception is thrown.
  282.      *
  283.      * @param selector the {@code NodeSelector}
  284.      * @return the {@code TrackedNodeData} object for this selector
  285.      * @throws ConfigurationRuntimeException if the selector cannot be resolved
  286.      */
  287.     private TrackedNodeData getTrackedNodeData(final NodeSelector selector) {
  288.         final TrackedNodeData trackData = trackedNodes.get(selector);
  289.         if (trackData == null) {
  290.             throw new ConfigurationRuntimeException("No tracked node found: " + selector);
  291.         }
  292.         return trackData;
  293.     }

  294.     /**
  295.      * Returns a flag whether the specified tracked node is detached.
  296.      *
  297.      * @param selector the {@code NodeSelector}
  298.      * @return a flag whether this node is detached
  299.      * @throws ConfigurationRuntimeException if no data for this selector is available
  300.      */
  301.     public boolean isTrackedNodeDetached(final NodeSelector selector) {
  302.         return getTrackedNodeData(selector).isDetached();
  303.     }

  304.     /**
  305.      * Replaces a tracked node by another one. This operation causes the tracked node to become detached.
  306.      *
  307.      * @param selector the {@code NodeSelector}
  308.      * @param newNode the replacement node
  309.      * @return the updated instance
  310.      * @throws ConfigurationRuntimeException if the selector cannot be resolved
  311.      */
  312.     public NodeTracker replaceAndDetachTrackedNode(final NodeSelector selector, final ImmutableNode newNode) {
  313.         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
  314.         newState.put(selector, getTrackedNodeData(selector).detach(newNode));
  315.         return new NodeTracker(newState);
  316.     }

  317.     /**
  318.      * Adds a node to be tracked. The passed in selector must select exactly one target node, otherwise an exception is
  319.      * thrown. A new instance is created with the updated tracking state.
  320.      *
  321.      * @param root the root node
  322.      * @param selector the {@code NodeSelector}
  323.      * @param resolver the {@code NodeKeyResolver}
  324.      * @param handler the {@code NodeHandler}
  325.      * @return the updated instance
  326.      * @throws ConfigurationRuntimeException if the selector does not select a single node
  327.      */
  328.     public NodeTracker trackNode(final ImmutableNode root, final NodeSelector selector, final NodeKeyResolver<ImmutableNode> resolver,
  329.         final NodeHandler<ImmutableNode> handler) {
  330.         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
  331.         final TrackedNodeData trackData = newState.get(selector);
  332.         newState.put(selector, trackDataForAddedObserver(root, selector, resolver, handler, trackData));
  333.         return new NodeTracker(newState);
  334.     }

  335.     /**
  336.      * Adds a number of nodes to be tracked. For each node in the passed in collection, a tracked node entry is created
  337.      * unless already one exists.
  338.      *
  339.      * @param selectors a collection with the {@code NodeSelector} objects
  340.      * @param nodes a collection with the nodes to be tracked
  341.      * @return the updated instance
  342.      */
  343.     public NodeTracker trackNodes(final Collection<NodeSelector> selectors, final Collection<ImmutableNode> nodes) {
  344.         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
  345.         final Iterator<ImmutableNode> itNodes = nodes.iterator();
  346.         selectors.forEach(selector -> {
  347.             final ImmutableNode node = itNodes.next();
  348.             TrackedNodeData trackData = newState.get(selector);
  349.             if (trackData == null) {
  350.                 trackData = new TrackedNodeData(node);
  351.             } else {
  352.                 trackData = trackData.observerAdded();
  353.             }
  354.             newState.put(selector, trackData);
  355.         });

  356.         return new NodeTracker(newState);
  357.     }

  358.     /**
  359.      * Notifies this object that an observer was removed for the specified tracked node. If this was the last observer, the
  360.      * track data for this selector can be removed.
  361.      *
  362.      * @param selector the {@code NodeSelector}
  363.      * @return the updated instance
  364.      * @throws ConfigurationRuntimeException if no information about this node is available
  365.      */
  366.     public NodeTracker untrackNode(final NodeSelector selector) {
  367.         final TrackedNodeData trackData = getTrackedNodeData(selector);

  368.         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
  369.         final TrackedNodeData newTrackData = trackData.observerRemoved();
  370.         if (newTrackData == null) {
  371.             newState.remove(selector);
  372.         } else {
  373.             newState.put(selector, newTrackData);
  374.         }
  375.         return new NodeTracker(newState);
  376.     }

  377.     /**
  378.      * Updates tracking information after the node structure has been changed. This method iterates over all tracked nodes.
  379.      * The selectors are evaluated again to update the node reference. If this fails for a selector, the previous node is
  380.      * reused; this tracked node is then detached. The passed in {@code NodeSelector} is the selector of the tracked node
  381.      * which is the target of the current transaction. (It is <strong>null</strong> if the transaction is not executed on a tracked
  382.      * node.) This is used to handle a special case: if the tracked node becomes detached by an operation targeting itself,
  383.      * this means that the node has been cleared by this operation. In this case, the previous node instance is not used,
  384.      * but an empty node is created.
  385.      *
  386.      * @param root the root node
  387.      * @param txTarget the {@code NodeSelector} referencing the target node of the current transaction (may be <strong>null</strong>)
  388.      * @param resolver the {@code NodeKeyResolver}
  389.      * @param handler the {@code NodeHandler}
  390.      * @return the updated instance
  391.      */
  392.     public NodeTracker update(final ImmutableNode root, final NodeSelector txTarget, final NodeKeyResolver<ImmutableNode> resolver,
  393.             final NodeHandler<ImmutableNode> handler) {
  394.         if (trackedNodes.isEmpty()) {
  395.             // there is not state to be updated
  396.             return this;
  397.         }

  398.         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>();
  399.         trackedNodes.entrySet().forEach(e -> newState.put(e.getKey(), determineUpdatedTrackedNodeData(root, txTarget, resolver, handler, e)));
  400.         return new NodeTracker(newState);
  401.     }
  402. }