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 }