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