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