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