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 }