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 }