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