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