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    *     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.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.SortedMap;
29  import java.util.TreeMap;
30  
31  /**
32   * <p>
33   * An internal helper class for a atomic updates of an {@link InMemoryNodeModel}.
34   * </p>
35   * <p>
36   * This class performs updates on the node structure of a node model consisting of {@link ImmutableNode} objects.
37   * Because the nodes themselves cannot be changed updates are achieved by replacing parts of the structure with new
38   * nodes; the new nodes are copies of original nodes with the corresponding manipulations applied. Therefore, each
39   * update of a node in the structure results in a new structure in which the affected node is replaced by a new one, and
40   * this change bubbles up to the root node (because all parent nodes have to be replaced by instances with an updated
41   * child reference).
42   * </p>
43   * <p>
44   * A single update of a model may consist of multiple changes on nodes. For instance, a remove property operation can
45   * include many nodes. There are some reasons why such updates should be handled in a single "transaction" rather than
46   * executing them on altered node structures one by one:
47   * <ul>
48   * <li>An operation is typically executed on a set of source nodes from the original node hierarchy. While manipulating
49   * nodes, nodes of this set may be replaced by new ones. The handling of these replacements complicates things a
50   * lot.</li>
51   * <li>Performing all updates one after the other may cause more updates of nodes than necessary. Nodes near to the root
52   * node always have to be replaced when a child of them gets manipulated. If all these updates are deferred and handled
53   * in a single transaction, the resulting operation is more efficient.</li>
54   * </ul>
55   * </p>
56   */
57  final class ModelTransaction {
58  
59      /**
60       * A specialized operation class for adding an attribute to a target node.
61       */
62      private static final class AddAttributeOperation extends Operation {
63  
64          /** The attribute name. */
65          private final String attributeName;
66  
67          /** The attribute value. */
68          private final Object attributeValue;
69  
70          /**
71           * Creates a new instance of {@code AddAttributeOperation}.
72           *
73           * @param name the name of the attribute
74           * @param value the value of the attribute
75           */
76          public AddAttributeOperation(final String name, final Object value) {
77              attributeName = name;
78              attributeValue = value;
79          }
80  
81          @Override
82          protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
83              return target.setAttribute(attributeName, attributeValue);
84          }
85      }
86  
87      /**
88       * A specialized operation class for adding multiple attributes to a target node.
89       */
90      private static final class AddAttributesOperation extends Operation {
91  
92          /** The map with attributes. */
93          private final Map<String, Object> attributes;
94  
95          /**
96           * Creates a new instance of {@code AddAttributesOperation}.
97           *
98           * @param attrs the map with attributes
99           */
100         public AddAttributesOperation(final Map<String, Object> attrs) {
101             attributes = attrs;
102         }
103 
104         @Override
105         protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
106             return target.setAttributes(attributes);
107         }
108     }
109 
110     /**
111      * A specialized operation class which changes the name of a node.
112      */
113     private static final class ChangeNodeNameOperation extends Operation {
114 
115         /** The new node name. */
116         private final String newName;
117 
118         /**
119          * Creates a new instance of {@code ChangeNodeNameOperation} and sets the new node name.
120          *
121          * @param name the new node name
122          */
123         public ChangeNodeNameOperation(final String name) {
124             newName = name;
125         }
126 
127         @Override
128         protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
129             return target.setName(newName);
130         }
131     }
132 
133     /**
134      * A specialized operation class which changes the value of a node.
135      */
136     private static final class ChangeNodeValueOperation extends Operation {
137 
138         /** The new value for the affected node. */
139         private final Object newValue;
140 
141         /**
142          * Creates a new instance of {@code ChangeNodeValueOperation} and initializes it with the new value to set for the node.
143          *
144          * @param value the new node value
145          */
146         public ChangeNodeValueOperation(final Object value) {
147             newValue = value;
148         }
149 
150         @Override
151         protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
152             return target.setValue(newValue);
153         }
154     }
155 
156     /**
157      * A specialized {@code Operation} implementation for replacing the children of a target node. All other properties are
158      * not touched. With this operation single children of a node can be altered or removed; new children can be added. This
159      * operation is frequently used because each update of a node causes updates of the children of all parent nodes.
160      * Therefore, it is treated in a special way and allows adding further sub operations dynamically.
161      */
162     private final class ChildrenUpdateOperation extends Operation {
163 
164         /** A collection with new nodes to be added. */
165         private Collection<ImmutableNode> newNodes;
166 
167         /** A collection with nodes to be removed. */
168         private Set<ImmutableNode> nodesToRemove;
169 
170         /**
171          * A map with nodes to be replaced by others. The keys are the nodes to be replaced, the values the replacements.
172          */
173         private Map<ImmutableNode, ImmutableNode> nodesToReplace;
174 
175         /**
176          * Adds a node to be added to the target of the operation.
177          *
178          * @param node the new node to be added
179          */
180         public void addNewNode(final ImmutableNode node) {
181             newNodes = append(newNodes, node);
182         }
183 
184         /**
185          * Adds a collection of nodes to be added to the target of the operation.
186          *
187          * @param nodes the collection with new nodes
188          */
189         public void addNewNodes(final Collection<? extends ImmutableNode> nodes) {
190             newNodes = concatenate(newNodes, nodes);
191         }
192 
193         /**
194          * Adds a node for a remove operation. This child node is going to be removed from its parent.
195          *
196          * @param node the child node to be removed
197          */
198         public void addNodeToRemove(final ImmutableNode node) {
199             nodesToRemove = append(nodesToRemove, node);
200         }
201 
202         /**
203          * Adds a node for a replacement operation. The original node is going to be replaced by its replacement.
204          *
205          * @param org the original node
206          * @param replacement the replacement node
207          */
208         public void addNodeToReplace(final ImmutableNode org, final ImmutableNode replacement) {
209             nodesToReplace = append(nodesToReplace, org, replacement);
210         }
211 
212         /**
213          * {@inheritDoc} This implementation applies changes on the children of the passed in target node according to its
214          * configuration: new nodes are added, replacements are performed, and nodes no longer needed are removed.
215          */
216         @Override
217         protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
218             final Map<ImmutableNode, ImmutableNode> replacements = fetchReplacementMap();
219             final Set<ImmutableNode> removals = fetchRemovalSet();
220             final List<ImmutableNode> resultNodes = new LinkedList<>();
221 
222             for (final ImmutableNode nd : target) {
223                 final ImmutableNode repl = replacements.get(nd);
224                 if (repl != null) {
225                     resultNodes.add(repl);
226                     replacedNodes.put(nd, repl);
227                 } else if (removals.contains(nd)) {
228                     removedNodes.add(nd);
229                 } else {
230                     resultNodes.add(nd);
231                 }
232             }
233 
234             concatenate(resultNodes, newNodes);
235             operations.newNodesAdded(newNodes);
236             return target.replaceChildren(resultNodes);
237         }
238 
239         /**
240          * Adds all operations defined by the specified object to this instance.
241          *
242          * @param op the operation to be combined
243          */
244         public void combine(final ChildrenUpdateOperation op) {
245             newNodes = concatenate(newNodes, op.newNodes);
246             nodesToReplace = concatenate(nodesToReplace, op.nodesToReplace);
247             nodesToRemove = concatenate(nodesToRemove, op.nodesToRemove);
248         }
249 
250         /**
251          * Returns a set with nodes to be removed. If no remove operations are pending, an empty set is returned.
252          *
253          * @return the set with nodes to be removed
254          */
255         private Set<ImmutableNode> fetchRemovalSet() {
256             return nodesToRemove != null ? nodesToRemove : Collections.<ImmutableNode>emptySet();
257         }
258 
259         /**
260          * Obtains the map with replacement nodes. If no replacements are defined, an empty map is returned.
261          *
262          * @return the map with replacement nodes
263          */
264         private Map<ImmutableNode, ImmutableNode> fetchReplacementMap() {
265             return nodesToReplace != null ? nodesToReplace : Collections.<ImmutableNode, ImmutableNode>emptyMap();
266         }
267     }
268 
269     /**
270      * An abstract base class representing an operation to be performed on a node. Concrete subclasses implement specific
271      * update operations.
272      */
273     private abstract static class Operation {
274 
275         /**
276          * Executes this operation on the provided target node returning the result.
277          *
278          * @param target the target node for this operation
279          * @param operations the current {@code Operations} instance
280          * @return the manipulated node
281          */
282         protected abstract ImmutableNode apply(ImmutableNode target, Operations operations);
283     }
284 
285     /**
286      * A helper class which collects multiple update operations to be executed on a single node.
287      */
288     private final class Operations {
289 
290         /** An operation for manipulating child nodes. */
291         private ChildrenUpdateOperation childrenOperation;
292 
293         /**
294          * A collection for the other operations to be performed on the target node.
295          */
296         private Collection<Operation> operations;
297 
298         /** A collection with nodes added by an operation. */
299         private Collection<ImmutableNode> addedNodesInOperation;
300 
301         /**
302          * Adds an operation which manipulates children.
303          *
304          * @param co the operation
305          */
306         public void addChildrenOperation(final ChildrenUpdateOperation co) {
307             if (childrenOperation == null) {
308                 childrenOperation = co;
309             } else {
310                 childrenOperation.combine(co);
311             }
312         }
313 
314         /**
315          * Adds an operation.
316          *
317          * @param op the operation
318          */
319         public void addOperation(final Operation op) {
320             operations = append(operations, op);
321         }
322 
323         /**
324          * Executes all operations stored in this object on the given target node. The resulting node then has to be integrated
325          * in the current node hierarchy. Unless the root node is already reached, this causes another updated operation to be
326          * created which replaces the manipulated child in the parent node.
327          *
328          * @param target the target node for this operation
329          * @param level the level of the target node
330          */
331         public void apply(final ImmutableNode target, final int level) {
332             ImmutableNode node = target;
333             if (childrenOperation != null) {
334                 node = childrenOperation.apply(node, this);
335             }
336 
337             if (operations != null) {
338                 for (final Operation op : operations) {
339                     node = op.apply(node, this);
340                 }
341             }
342 
343             handleAddedNodes(node);
344             if (level == 0) {
345                 // reached the root node
346                 newRoot = node;
347                 replacedNodes.put(target, node);
348             } else {
349                 // propagate change
350                 propagateChange(target, node, level);
351             }
352         }
353 
354         /**
355          * Checks whether new nodes have been added during operation execution. If so, the parent mapping has to be updated.
356          *
357          * @param node the resulting node after applying all operations
358          */
359         private void handleAddedNodes(final ImmutableNode node) {
360             if (addedNodesInOperation != null) {
361                 addedNodesInOperation.forEach(child -> {
362                     parentMapping.put(child, node);
363                     addedNodes.add(child);
364                 });
365             }
366         }
367 
368         /**
369          * Notifies this object that new nodes have been added by a sub operation. It has to be ensured that these nodes are
370          * added to the parent mapping.
371          *
372          * @param newNodes the collection of newly added nodes
373          */
374         public void newNodesAdded(final Collection<ImmutableNode> newNodes) {
375             addedNodesInOperation = concatenate(addedNodesInOperation, newNodes);
376         }
377 
378         /**
379          * Propagates the changes on the target node to the next level above of the hierarchy. If the updated node is no longer
380          * defined, it can even be removed from its parent. Otherwise, it is just replaced.
381          *
382          * @param target the target node for this operation
383          * @param node the resulting node after applying all operations
384          * @param level the level of the target node
385          */
386         private void propagateChange(final ImmutableNode target, final ImmutableNode node, final int level) {
387             final ImmutableNode parent = getParent(target);
388             final ChildrenUpdateOperation co = new ChildrenUpdateOperation();
389             if (InMemoryNodeModel.checkIfNodeDefined(node)) {
390                 co.addNodeToReplace(target, node);
391             } else {
392                 co.addNodeToRemove(target);
393             }
394             fetchOperations(parent, level - 1).addChildrenOperation(co);
395         }
396     }
397 
398     /**
399      * A specialized operation class for removing an attribute from a target node.
400      */
401     private static final class RemoveAttributeOperation extends Operation {
402 
403         /** The attribute name. */
404         private final String attributeName;
405 
406         /**
407          * Creates a new instance of {@code RemoveAttributeOperation}.
408          *
409          * @param name the name of the attribute
410          */
411         public RemoveAttributeOperation(final String name) {
412             attributeName = name;
413         }
414 
415         @Override
416         protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
417             return target.removeAttribute(attributeName);
418         }
419     }
420 
421     /**
422      * Constant for the maximum number of entries in the replacement mapping. If this number is exceeded, the parent mapping
423      * is reconstructed. The number is a bit arbitrary. If it is too low, updates - especially on large node structures -
424      * are expensive because the parent mapping is often rebuild. If it is too big, read access to the model is slowed down
425      * because looking up the parent of a node is more complicated.
426      */
427     private static final int MAX_REPLACEMENTS = 200;
428 
429     /** Constant for an unknown level. */
430     private static final int LEVEL_UNKNOWN = -1;
431 
432     /**
433      * Appends a single element to a collection. The collection may be null, then it is created.
434      *
435      * @param col the collection
436      * @param node the element to be added
437      * @param <E> the type of elements involved
438      * @return the resulting collection
439      */
440     private static <E> Collection<E> append(final Collection<E> col, final E node) {
441         final Collection<E> result = col != null ? col : new LinkedList<>();
442         result.add(node);
443         return result;
444     }
445 
446     /**
447      * Adds a single key-value pair to a map. The map may be null, then it is created.
448      *
449      * @param map the map
450      * @param key the key
451      * @param value the value
452      * @param <K> the type of the key
453      * @param <V> the type of the value
454      * @return the resulting map
455      */
456     private static <K, V> Map<K, V> append(final Map<K, V> map, final K key, final V value) {
457         final Map<K, V> result = map != null ? map : new HashMap<>();
458         result.put(key, value);
459         return result;
460     }
461 
462     /**
463      * Appends a single element to a set. The set may be null then it is created.
464      *
465      * @param col the set
466      * @param elem the element to be added
467      * @param <E> the type of the elements involved
468      * @return the resulting set
469      */
470     private static <E> Set<E> append(final Set<E> col, final E elem) {
471         final Set<E> result = col != null ? col : new HashSet<>();
472         result.add(elem);
473         return result;
474     }
475 
476     /**
477      * Constructs the concatenation of two collections. Both can be null.
478      *
479      * @param col1 the first collection
480      * @param col2 the second collection
481      * @param <E> the type of the elements involved
482      * @return the resulting collection
483      */
484     private static <E> Collection<E> concatenate(final Collection<E> col1, final Collection<? extends E> col2) {
485         if (col2 == null) {
486             return col1;
487         }
488 
489         final Collection<E> result = col1 != null ? col1 : new ArrayList<>(col2.size());
490         result.addAll(col2);
491         return result;
492     }
493 
494     /**
495      * Constructs the concatenation of two maps. Both can be null.
496      *
497      * @param map1 the first map
498      * @param map2 the second map
499      * @param <K> the type of the keys
500      * @param <V> the type of the values
501      * @return the resulting map
502      */
503     private static <K, V> Map<K, V> concatenate(final Map<K, V> map1, final Map<? extends K, ? extends V> map2) {
504         if (map2 == null) {
505             return map1;
506         }
507 
508         final Map<K, V> result = map1 != null ? map1 : new HashMap<>();
509         result.putAll(map2);
510         return result;
511     }
512 
513     /**
514      * Constructs the concatenation of two sets. Both can be null.
515      *
516      * @param set1 the first set
517      * @param set2 the second set
518      * @param <E> the type of the elements involved
519      * @return the resulting set
520      */
521     private static <E> Set<E> concatenate(final Set<E> set1, final Set<? extends E> set2) {
522         if (set2 == null) {
523             return set1;
524         }
525 
526         final Set<E> result = set1 != null ? set1 : new HashSet<>();
527         result.addAll(set2);
528         return result;
529     }
530 
531     /** Stores the current tree data of the calling node model. */
532     private final TreeData currentData;
533 
534     /** The root node for query operations. */
535     private final ImmutableNode queryRoot;
536 
537     /** The selector to the root node of this transaction. */
538     private final NodeSelector rootNodeSelector;
539 
540     /** The {@code NodeKeyResolver} to be used for this transaction. */
541     private final NodeKeyResolver<ImmutableNode> resolver;
542 
543     /** A new replacement mapping. */
544     private final Map<ImmutableNode, ImmutableNode> replacementMapping;
545 
546     /** The nodes replaced in this transaction. */
547     private final Map<ImmutableNode, ImmutableNode> replacedNodes;
548 
549     /** A new parent mapping. */
550     private final Map<ImmutableNode, ImmutableNode> parentMapping;
551 
552     /** A collection with nodes which have been added. */
553     private final Collection<ImmutableNode> addedNodes;
554 
555     /** A collection with nodes which have been removed. */
556     private final Collection<ImmutableNode> removedNodes;
557 
558     /**
559      * Stores all nodes which have been removed in this transaction (not only the root nodes of removed trees).
560      */
561     private final Collection<ImmutableNode> allRemovedNodes;
562 
563     /**
564      * Stores the operations to be executed during this transaction. The map is sorted by the levels of the nodes to be
565      * manipulated: Operations on nodes down in the hierarchy are executed first because they affect the nodes closer to the
566      * root.
567      */
568     private final SortedMap<Integer, Map<ImmutableNode, Operations>> operations;
569 
570     /** A map with reference objects to be added during this transaction. */
571     private Map<ImmutableNode, Object> newReferences;
572 
573     /** The new root node. */
574     private ImmutableNode newRoot;
575 
576     /**
577      * Creates a new instance of {@code ModelTransaction} for the current tree data.
578      *
579      * @param treeData the current {@code TreeData} structure to operate on
580      * @param selector an optional {@code NodeSelector} defining the target root node for this transaction; this can be used
581      *        to perform operations on tracked nodes
582      * @param resolver the {@code NodeKeyResolver}
583      */
584     public ModelTransaction(final TreeData treeData, final NodeSelector selector, final NodeKeyResolver<ImmutableNode> resolver) {
585         currentData = treeData;
586         this.resolver = resolver;
587         replacementMapping = getCurrentData().copyReplacementMapping();
588         replacedNodes = new HashMap<>();
589         parentMapping = getCurrentData().copyParentMapping();
590         operations = new TreeMap<>();
591         addedNodes = new LinkedList<>();
592         removedNodes = new LinkedList<>();
593         allRemovedNodes = new LinkedList<>();
594         queryRoot = initQueryRoot(treeData, selector);
595         rootNodeSelector = selector;
596     }
597 
598     /**
599      * Adds an operation for adding a new child to a given parent node.
600      *
601      * @param parent the parent node
602      * @param newChild the new child to be added
603      */
604     public void addAddNodeOperation(final ImmutableNode parent, final ImmutableNode newChild) {
605         final ChildrenUpdateOperation op = new ChildrenUpdateOperation();
606         op.addNewNode(newChild);
607         fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op);
608     }
609 
610     /**
611      * Adds an operation for adding a number of new children to a given parent node.
612      *
613      * @param parent the parent node
614      * @param newNodes the collection of new child nodes
615      */
616     public void addAddNodesOperation(final ImmutableNode parent, final Collection<? extends ImmutableNode> newNodes) {
617         final ChildrenUpdateOperation op = new ChildrenUpdateOperation();
618         op.addNewNodes(newNodes);
619         fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op);
620     }
621 
622     /**
623      * Adds an operation for adding an attribute to a target node.
624      *
625      * @param target the target node
626      * @param name the name of the attribute
627      * @param value the value of the attribute
628      */
629     public void addAttributeOperation(final ImmutableNode target, final String name, final Object value) {
630         fetchOperations(target, LEVEL_UNKNOWN).addOperation(new AddAttributeOperation(name, value));
631     }
632 
633     /**
634      * Adds an operation for adding multiple attributes to a target node.
635      *
636      * @param target the target node
637      * @param attributes the map with attributes to be set
638      */
639     public void addAttributesOperation(final ImmutableNode target, final Map<String, Object> attributes) {
640         fetchOperations(target, LEVEL_UNKNOWN).addOperation(new AddAttributesOperation(attributes));
641     }
642 
643     /**
644      * Adds an operation for changing the name of a target node.
645      *
646      * @param target the target node
647      * @param newName the new name for this node
648      */
649     public void addChangeNodeNameOperation(final ImmutableNode target, final String newName) {
650         fetchOperations(target, LEVEL_UNKNOWN).addOperation(new ChangeNodeNameOperation(newName));
651     }
652 
653     /**
654      * Adds an operation for changing the value of a target node.
655      *
656      * @param target the target node
657      * @param newValue the new value for this node
658      */
659     public void addChangeNodeValueOperation(final ImmutableNode target, final Object newValue) {
660         fetchOperations(target, LEVEL_UNKNOWN).addOperation(new ChangeNodeValueOperation(newValue));
661     }
662 
663     /**
664      * Adds an operation for clearing the value of a target node.
665      *
666      * @param target the target node
667      */
668     public void addClearNodeValueOperation(final ImmutableNode target) {
669         addChangeNodeValueOperation(target, null);
670     }
671 
672     /**
673      * Adds a new reference object for the given node.
674      *
675      * @param node the affected node
676      * @param ref the reference object for this node
677      */
678     public void addNewReference(final ImmutableNode node, final Object ref) {
679         fetchReferenceMap().put(node, ref);
680     }
681 
682     /**
683      * Adds a map with new reference objects. The entries in this map are passed to the {@code ReferenceTracker} during
684      * execution of this transaction.
685      *
686      * @param refs the map with new reference objects
687      */
688     public void addNewReferences(final Map<ImmutableNode, ?> refs) {
689         fetchReferenceMap().putAll(refs);
690     }
691 
692     /**
693      * Adds an operation for removing an attribute from a target node.
694      *
695      * @param target the target node
696      * @param name the name of the attribute
697      */
698     public void addRemoveAttributeOperation(final ImmutableNode target, final String name) {
699         fetchOperations(target, LEVEL_UNKNOWN).addOperation(new RemoveAttributeOperation(name));
700     }
701 
702     /**
703      * Adds an operation for removing a child node of a given node.
704      *
705      * @param parent the parent node
706      * @param node the child node to be removed
707      */
708     public void addRemoveNodeOperation(final ImmutableNode parent, final ImmutableNode node) {
709         final ChildrenUpdateOperation op = new ChildrenUpdateOperation();
710         op.addNodeToRemove(node);
711         fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op);
712     }
713 
714     /**
715      * Executes this transaction resulting in a new {@code TreeData} object. The object returned by this method serves as
716      * the definition of a new node structure for the calling model.
717      *
718      * @return the updated {@code TreeData}
719      */
720     public TreeData execute() {
721         executeOperations();
722         updateParentMapping();
723         return new TreeData(newRoot, parentMapping, replacementMapping,
724             currentData.getNodeTracker().update(newRoot, rootNodeSelector, getResolver(), getCurrentData()), updateReferenceTracker());
725     }
726 
727     /**
728      * Executes all operations in this transaction.
729      */
730     private void executeOperations() {
731         while (!operations.isEmpty()) {
732             final Integer level = operations.lastKey(); // start down in hierarchy
733             operations.remove(level).forEach((k, v) -> v.apply(k, level));
734         }
735     }
736 
737     /**
738      * Obtains the {@code Operations} object for manipulating the specified node. If no such object exists yet, it is
739      * created. The level can be undefined, then it is determined based on the target node.
740      *
741      * @param target the target node
742      * @param level the level of the target node (may be undefined)
743      * @return the {@code Operations} object for this node
744      */
745     Operations fetchOperations(final ImmutableNode target, final int level) {
746         final Integer nodeLevel = Integer.valueOf(level == LEVEL_UNKNOWN ? level(target) : level);
747         final Map<ImmutableNode, Operations> levelOperations = operations.computeIfAbsent(nodeLevel, k -> new HashMap<>());
748         return levelOperations.computeIfAbsent(target, k -> new Operations());
749     }
750 
751     /**
752      * Returns the map with new reference objects. It is created if necessary.
753      *
754      * @return the map with reference objects
755      */
756     private Map<ImmutableNode, Object> fetchReferenceMap() {
757         if (newReferences == null) {
758             newReferences = new HashMap<>();
759         }
760         return newReferences;
761     }
762 
763     /**
764      * Gets the current {@code TreeData} object this transaction operates on.
765      *
766      * @return the associated {@code TreeData} object
767      */
768     public TreeData getCurrentData() {
769         return currentData;
770     }
771 
772     /**
773      * Gets the parent node of the given node.
774      *
775      * @param node the node in question
776      * @return the parent of this node
777      */
778     ImmutableNode getParent(final ImmutableNode node) {
779         return getCurrentData().getParent(node);
780     }
781 
782     /**
783      * Gets the root node to be used within queries. This is not necessarily the current root node of the model. If the
784      * operation is executed on a tracked node, this node has to be passed as root nodes to the expression engine.
785      *
786      * @return the root node for queries and calls to the expression engine
787      */
788     public ImmutableNode getQueryRoot() {
789         return queryRoot;
790     }
791 
792     /**
793      * Gets the {@code NodeKeyResolver} used by this transaction.
794      *
795      * @return the {@code NodeKeyResolver}
796      */
797     public NodeKeyResolver<ImmutableNode> getResolver() {
798         return resolver;
799     }
800 
801     /**
802      * Initializes the root node to be used within queries. If a tracked node selector is provided, this node becomes the
803      * root node. Otherwise, the actual root node is used.
804      *
805      * @param treeData the current data of the model
806      * @param selector an optional {@code NodeSelector} defining the target root
807      * @return the query root node for this transaction
808      */
809     private ImmutableNode initQueryRoot(final TreeData treeData, final NodeSelector selector) {
810         return selector == null ? treeData.getRootNode() : treeData.getNodeTracker().getTrackedNode(selector);
811     }
812 
813     /**
814      * Determines the level of the specified node in the current hierarchy. The level of the root node is 0, the children of
815      * the root have level 1 and so on.
816      *
817      * @param node the node in question
818      * @return the level of this node
819      */
820     private int level(final ImmutableNode node) {
821         ImmutableNode current = getCurrentData().getParent(node);
822         int level = 0;
823         while (current != null) {
824             level++;
825             current = getCurrentData().getParent(current);
826         }
827         return level;
828     }
829 
830     /**
831      * Rebuilds the parent mapping from scratch. This method is called if the replacement mapping exceeds its maximum size.
832      * In this case, it is cleared, and a new parent mapping is constructed for the new root node.
833      */
834     private void rebuildParentMapping() {
835         replacementMapping.clear();
836         parentMapping.clear();
837         InMemoryNodeModel.updateParentMapping(parentMapping, newRoot);
838     }
839 
840     /**
841      * Removes the specified node completely from the replacement mapping. This also includes the nodes that replace the
842      * given one.
843      *
844      * @param node the node to be removed
845      */
846     private void removeNodeFromReplacementMapping(final ImmutableNode node) {
847         ImmutableNode replacement = node;
848         do {
849             replacement = replacementMapping.remove(replacement);
850         } while (replacement != null);
851     }
852 
853     /**
854      * Removes a node and its children (recursively) from the parent and the replacement mappings.
855      *
856      * @param root the root of the subtree to be removed
857      */
858     private void removeNodesFromParentAndReplacementMapping(final ImmutableNode root) {
859         NodeTreeWalker.INSTANCE.walkBFS(root, new ConfigurationNodeVisitorAdapter<ImmutableNode>() {
860             @Override
861             public void visitBeforeChildren(final ImmutableNode node, final NodeHandler<ImmutableNode> handler) {
862                 allRemovedNodes.add(node);
863                 parentMapping.remove(node);
864                 removeNodeFromReplacementMapping(node);
865             }
866         }, getCurrentData());
867     }
868 
869     /**
870      * Updates the parent mapping for the resulting {@code TreeData} instance. This method is called after all update
871      * operations have been executed. It ensures that the parent mapping is updated for the changes on the nodes structure.
872      */
873     private void updateParentMapping() {
874         replacementMapping.putAll(replacedNodes);
875         if (replacementMapping.size() > MAX_REPLACEMENTS) {
876             rebuildParentMapping();
877         } else {
878             updateParentMappingForAddedNodes();
879             updateParentMappingForRemovedNodes();
880         }
881     }
882 
883     /**
884      * Adds newly added nodes and their children to the parent mapping.
885      */
886     private void updateParentMappingForAddedNodes() {
887         addedNodes.forEach(node -> InMemoryNodeModel.updateParentMapping(parentMapping, node));
888     }
889 
890     /**
891      * Removes nodes that have been removed during this transaction from the parent and replacement mappings.
892      */
893     private void updateParentMappingForRemovedNodes() {
894         removedNodes.forEach(this::removeNodesFromParentAndReplacementMapping);
895     }
896 
897     /**
898      * Returns an updated {@code ReferenceTracker} instance. The changes performed during this transaction are applied to
899      * the tracker.
900      *
901      * @return the updated tracker instance
902      */
903     private ReferenceTracker updateReferenceTracker() {
904         ReferenceTracker tracker = currentData.getReferenceTracker();
905         if (newReferences != null) {
906             tracker = tracker.addReferences(newReferences);
907         }
908         return tracker.updateReferences(replacedNodes, allRemovedNodes);
909     }
910 }