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 * 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 }