View Javadoc
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  
19  import java.util.Collection;
20  import java.util.Collections;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.Map;
24  import java.util.stream.Collectors;
25  
26  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
27  
28  /**
29   * <p>
30   * A class which can track specific nodes in an {@link InMemoryNodeModel}.
31   * </p>
32   * <p>
33   * Sometimes it is necessary to keep track on a specific node, for instance when operating on a subtree of a model. For
34   * a model comprised of immutable nodes this is not trivial because each update of the model may cause the node to be
35   * replaced. So holding a direct pointer onto the target node is not an option; this instance may become outdated.
36   * </p>
37   * <p>
38   * This class provides an API for selecting a specific node by using a {@link NodeSelector}. The selector is used to
39   * obtain an initial reference to the target node. It is then applied again after each update of the associated node
40   * model (which is done in the {@code update()} method). At this point of time two things can happen:
41   * <ul>
42   * <li>The {@code NodeSelector} associated with the tracked node still selects a single node. Then this node becomes the
43   * new tracked node. This may be the same instance as before or a new one.</li>
44   * <li>The selector does no longer find the target node. This can happen for instance if it has been removed by an
45   * operation. In this case, the previous node instance is used. It is now detached from the model, but can still be used
46   * for operations on this subtree. It may even become life again after another update of the model.</li>
47   * </ul>
48   * </p>
49   * <p>
50   * Implementation note: This class is intended to work in a concurrent environment. Instances are immutable. The
51   * represented state can be updated by creating new instances which are then stored by the owning node model.
52   * </p>
53   *
54   * @since 2.0
55   */
56  final class NodeTracker {
57      /** A map with data about tracked nodes. */
58      private final Map<NodeSelector, TrackedNodeData> trackedNodes;
59  
60      /**
61       * Creates a new instance of {@code NodeTracker}. This instance does not yet track any nodes.
62       */
63      public NodeTracker() {
64          this(Collections.<NodeSelector, TrackedNodeData>emptyMap());
65      }
66  
67      /**
68       * Creates a new instance of {@code NodeTracker} and initializes it with the given map of tracked nodes. This
69       * constructor is used internally when the state of tracked nodes has changed.
70       *
71       * @param map the map with tracked nodes
72       */
73      private NodeTracker(final Map<NodeSelector, TrackedNodeData> map) {
74          trackedNodes = map;
75      }
76  
77      /**
78       * Adds a node to be tracked. The passed in selector must select exactly one target node, otherwise an exception is
79       * thrown. A new instance is created with the updated tracking state.
80       *
81       * @param root the root node
82       * @param selector the {@code NodeSelector}
83       * @param resolver the {@code NodeKeyResolver}
84       * @param handler the {@code NodeHandler}
85       * @return the updated instance
86       * @throws ConfigurationRuntimeException if the selector does not select a single node
87       */
88      public NodeTracker trackNode(final ImmutableNode root, final NodeSelector selector, final NodeKeyResolver<ImmutableNode> resolver,
89          final NodeHandler<ImmutableNode> handler) {
90          final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
91          final TrackedNodeData trackData = newState.get(selector);
92          newState.put(selector, trackDataForAddedObserver(root, selector, resolver, handler, trackData));
93          return new NodeTracker(newState);
94      }
95  
96      /**
97       * Adds a number of nodes to be tracked. For each node in the passed in collection, a tracked node entry is created
98       * unless already one exists.
99       *
100      * @param selectors a collection with the {@code NodeSelector} objects
101      * @param nodes a collection with the nodes to be tracked
102      * @return the updated instance
103      */
104     public NodeTracker trackNodes(final Collection<NodeSelector> selectors, final Collection<ImmutableNode> nodes) {
105         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
106         final Iterator<ImmutableNode> itNodes = nodes.iterator();
107         selectors.forEach(selector -> {
108             final ImmutableNode node = itNodes.next();
109             TrackedNodeData trackData = newState.get(selector);
110             if (trackData == null) {
111                 trackData = new TrackedNodeData(node);
112             } else {
113                 trackData = trackData.observerAdded();
114             }
115             newState.put(selector, trackData);
116         });
117 
118         return new NodeTracker(newState);
119     }
120 
121     /**
122      * Notifies this object that an observer was removed for the specified tracked node. If this was the last observer, the
123      * track data for this selector can be removed.
124      *
125      * @param selector the {@code NodeSelector}
126      * @return the updated instance
127      * @throws ConfigurationRuntimeException if no information about this node is available
128      */
129     public NodeTracker untrackNode(final NodeSelector selector) {
130         final TrackedNodeData trackData = getTrackedNodeData(selector);
131 
132         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
133         final TrackedNodeData newTrackData = trackData.observerRemoved();
134         if (newTrackData == null) {
135             newState.remove(selector);
136         } else {
137             newState.put(selector, newTrackData);
138         }
139         return new NodeTracker(newState);
140     }
141 
142     /**
143      * Gets the current {@code ImmutableNode} instance associated with the given selector.
144      *
145      * @param selector the {@code NodeSelector}
146      * @return the {@code ImmutableNode} selected by this selector
147      * @throws ConfigurationRuntimeException if no data for this selector is available
148      */
149     public ImmutableNode getTrackedNode(final NodeSelector selector) {
150         return getTrackedNodeData(selector).getNode();
151     }
152 
153     /**
154      * Returns a flag whether the specified tracked node is detached.
155      *
156      * @param selector the {@code NodeSelector}
157      * @return a flag whether this node is detached
158      * @throws ConfigurationRuntimeException if no data for this selector is available
159      */
160     public boolean isTrackedNodeDetached(final NodeSelector selector) {
161         return getTrackedNodeData(selector).isDetached();
162     }
163 
164     /**
165      * Gets the detached node model for the specified tracked node. When a node becomes detached, operations on it are
166      * independent from the original model. To implement this, a separate node model is created wrapping this tracked node.
167      * This model can be queried by this method. If the node affected is not detached, result is <b>null</b>.
168      *
169      * @param selector the {@code NodeSelector}
170      * @return the detached node model for this node or <b>null</b>
171      * @throws ConfigurationRuntimeException if no data for this selector is available
172      */
173     public InMemoryNodeModel getDetachedNodeModel(final NodeSelector selector) {
174         return getTrackedNodeData(selector).getDetachedModel();
175     }
176 
177     /**
178      * Updates tracking information after the node structure has been changed. This method iterates over all tracked nodes.
179      * The selectors are evaluated again to update the node reference. If this fails for a selector, the previous node is
180      * reused; this tracked node is then detached. The passed in {@code NodeSelector} is the selector of the tracked node
181      * which is the target of the current transaction. (It is <b>null</b> if the transaction is not executed on a tracked
182      * node.) This is used to handle a special case: if the tracked node becomes detached by an operation targeting itself,
183      * this means that the node has been cleared by this operation. In this case, the previous node instance is not used,
184      * but an empty node is created.
185      *
186      * @param root the root node
187      * @param txTarget the {@code NodeSelector} referencing the target node of the current transaction (may be <b>null</b>)
188      * @param resolver the {@code NodeKeyResolver}
189      * @param handler the {@code NodeHandler}
190      * @return the updated instance
191      */
192     public NodeTracker update(final ImmutableNode root, final NodeSelector txTarget, final NodeKeyResolver<ImmutableNode> resolver,
193             final NodeHandler<ImmutableNode> handler) {
194         if (trackedNodes.isEmpty()) {
195             // there is not state to be updated
196             return this;
197         }
198 
199         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>();
200         trackedNodes.entrySet().forEach(e -> newState.put(e.getKey(), determineUpdatedTrackedNodeData(root, txTarget, resolver, handler, e)));
201         return new NodeTracker(newState);
202     }
203 
204     /**
205      * Marks all tracked nodes as detached. This method is called if there are some drastic changes on the underlying node
206      * structure, e.g. if the root node was replaced.
207      *
208      * @return the updated instance
209      */
210     public NodeTracker detachAllTrackedNodes() {
211         if (trackedNodes.isEmpty()) {
212             // there is not state to be updated
213             return this;
214         }
215         return new NodeTracker(trackedNodes.entrySet().stream()
216             .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().isDetached() ? e.getValue() : e.getValue().detach(null))));
217     }
218 
219     /**
220      * Replaces a tracked node by another one. This operation causes the tracked node to become detached.
221      *
222      * @param selector the {@code NodeSelector}
223      * @param newNode the replacement node
224      * @return the updated instance
225      * @throws ConfigurationRuntimeException if the selector cannot be resolved
226      */
227     public NodeTracker replaceAndDetachTrackedNode(final NodeSelector selector, final ImmutableNode newNode) {
228         final Map<NodeSelector, TrackedNodeData> newState = new HashMap<>(trackedNodes);
229         newState.put(selector, getTrackedNodeData(selector).detach(newNode));
230         return new NodeTracker(newState);
231     }
232 
233     /**
234      * Obtains the {@code TrackedNodeData} object for the specified selector. If the selector cannot be resolved, an
235      * exception is thrown.
236      *
237      * @param selector the {@code NodeSelector}
238      * @return the {@code TrackedNodeData} object for this selector
239      * @throws ConfigurationRuntimeException if the selector cannot be resolved
240      */
241     private TrackedNodeData getTrackedNodeData(final NodeSelector selector) {
242         final TrackedNodeData trackData = trackedNodes.get(selector);
243         if (trackData == null) {
244             throw new ConfigurationRuntimeException("No tracked node found: " + selector);
245         }
246         return trackData;
247     }
248 
249     /**
250      * Returns a {@code TrackedNodeData} object for an update operation. If the tracked node is still life, its selector is
251      * applied to the current root node. It may become detached if there is no match.
252      *
253      * @param root the root node
254      * @param txTarget the {@code NodeSelector} referencing the target node of the current transaction (may be <b>null</b>)
255      * @param resolver the {@code NodeKeyResolver}
256      * @param handler the {@code NodeHandler}
257      * @param e the current selector and {@code TrackedNodeData}
258      * @return the updated {@code TrackedNodeData}
259      */
260     private static TrackedNodeData determineUpdatedTrackedNodeData(final ImmutableNode root, final NodeSelector txTarget,
261         final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler, final Map.Entry<NodeSelector, TrackedNodeData> e) {
262         if (e.getValue().isDetached()) {
263             return e.getValue();
264         }
265 
266         ImmutableNode newTarget;
267         try {
268             newTarget = e.getKey().select(root, resolver, handler);
269         } catch (final Exception ex) {
270             /*
271              * Evaluation of the key caused an exception. This can happen for instance if the expression engine was changed. In this
272              * case, the node becomes detached.
273              */
274             newTarget = null;
275         }
276         if (newTarget == null) {
277             return detachedTrackedNodeData(txTarget, e);
278         }
279         return e.getValue().updateNode(newTarget);
280     }
281 
282     /**
283      * Creates a new {@code TrackedNodeData} object for a tracked node which becomes detached within the current
284      * transaction. This method checks whether the affected node is the root node of the current transaction. If so, it is
285      * cleared.
286      *
287      * @param txTarget the {@code NodeSelector} referencing the target node of the current transaction (may be <b>null</b>)
288      * @param e the current selector and {@code TrackedNodeData}
289      * @return the new {@code TrackedNodeData} object to be used for this tracked node
290      */
291     private static TrackedNodeData detachedTrackedNodeData(final NodeSelector txTarget, final Map.Entry<NodeSelector, TrackedNodeData> e) {
292         final ImmutableNode newNode = e.getKey().equals(txTarget) ? createEmptyTrackedNode(e.getValue()) : null;
293         return e.getValue().detach(newNode);
294     }
295 
296     /**
297      * Creates an empty node derived from the passed in {@code TrackedNodeData} object. This method is called if a tracked
298      * node got cleared by a transaction.
299      *
300      * @param data the {@code TrackedNodeData}
301      * @return the new node instance for this tracked node
302      */
303     private static ImmutableNode createEmptyTrackedNode(final TrackedNodeData data) {
304         return new ImmutableNode.Builder().name(data.getNode().getNodeName()).create();
305     }
306 
307     /**
308      * Creates a {@code TrackedNodeData} object for a newly added observer for the specified node selector.
309      *
310      * @param root the root node
311      * @param selector the {@code NodeSelector}
312      * @param resolver the {@code NodeKeyResolver}
313      * @param handler the {@code NodeHandler}
314      * @param trackData the current data for this selector
315      * @return the updated {@code TrackedNodeData}
316      * @throws ConfigurationRuntimeException if the selector does not select a single node
317      */
318     private static TrackedNodeData trackDataForAddedObserver(final ImmutableNode root, final NodeSelector selector,
319         final NodeKeyResolver<ImmutableNode> resolver, final NodeHandler<ImmutableNode> handler, final TrackedNodeData trackData) {
320         if (trackData != null) {
321             return trackData.observerAdded();
322         }
323         final ImmutableNode target = selector.select(root, resolver, handler);
324         if (target == null) {
325             throw new ConfigurationRuntimeException("Selector does not select unique node: " + selector);
326         }
327         return new TrackedNodeData(target);
328     }
329 
330     /**
331      * A simple data class holding information about a tracked node.
332      */
333     private static final class TrackedNodeData {
334         /** The current instance of the tracked node. */
335         private final ImmutableNode node;
336 
337         /** The number of observers of this tracked node. */
338         private final int observerCount;
339 
340         /** A node model to be used when the tracked node is detached. */
341         private final InMemoryNodeModel detachedModel;
342 
343         /**
344          * Creates a new instance of {@code TrackedNodeData} and initializes it with the current reference to the tracked node.
345          *
346          * @param nd the tracked node
347          */
348         public TrackedNodeData(final ImmutableNode nd) {
349             this(nd, 1, null);
350         }
351 
352         /**
353          * Creates a new instance of {@code TrackedNodeData} and initializes its properties.
354          *
355          * @param nd the tracked node
356          * @param obsCount the observer count
357          * @param detachedNodeModel a model to be used in detached mode
358          */
359         private TrackedNodeData(final ImmutableNode nd, final int obsCount, final InMemoryNodeModel detachedNodeModel) {
360             node = nd;
361             observerCount = obsCount;
362             detachedModel = detachedNodeModel;
363         }
364 
365         /**
366          * Gets the tracked node.
367          *
368          * @return the tracked node
369          */
370         public ImmutableNode getNode() {
371             return getDetachedModel() != null ? getDetachedModel().getRootNode() : node;
372         }
373 
374         /**
375          * Gets the node model to be used in detached mode. This is <b>null</b> if the represented tracked node is not
376          * detached.
377          *
378          * @return the node model in detached mode
379          */
380         public InMemoryNodeModel getDetachedModel() {
381             return detachedModel;
382         }
383 
384         /**
385          * Returns a flag whether the represented tracked node is detached.
386          *
387          * @return the detached flag
388          */
389         public boolean isDetached() {
390             return getDetachedModel() != null;
391         }
392 
393         /**
394          * Another observer was added for this tracked node. This method returns a new instance with an adjusted observer count.
395          *
396          * @return the updated instance
397          */
398         public TrackedNodeData observerAdded() {
399             return new TrackedNodeData(node, observerCount + 1, getDetachedModel());
400         }
401 
402         /**
403          * An observer for this tracked node was removed. This method returns a new instance with an adjusted observer count. If
404          * there are no more observers, result is <b>null</b>. This means that this node is no longer tracked and can be
405          * released.
406          *
407          * @return the updated instance or <b>null</b>
408          */
409         public TrackedNodeData observerRemoved() {
410             return observerCount <= 1 ? null : new TrackedNodeData(node, observerCount - 1, getDetachedModel());
411         }
412 
413         /**
414          * Updates the node reference. This method is called after an update of the underlying node structure if the tracked
415          * node was replaced by another instance.
416          *
417          * @param newNode the new tracked node instance
418          * @return the updated instance
419          */
420         public TrackedNodeData updateNode(final ImmutableNode newNode) {
421             return new TrackedNodeData(newNode, observerCount, getDetachedModel());
422         }
423 
424         /**
425          * Returns an instance with the detached flag set to true. This method is called if the selector of a tracked node does
426          * not match a single node any more. It is possible to pass in a new node instance which becomes the current tracked
427          * node. If this is <b>null</b>, the previous node instance is used.
428          *
429          * @param newNode the new tracked node instance (may be <b>null</b>)
430          * @return the updated instance
431          */
432         public TrackedNodeData detach(final ImmutableNode newNode) {
433             final ImmutableNode newTrackedNode = newNode != null ? newNode : getNode();
434             return new TrackedNodeData(newTrackedNode, observerCount, new InMemoryNodeModel(newTrackedNode));
435         }
436     }
437 }