| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.configuration2.tree; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedMap; |
| import java.util.TreeMap; |
| |
| /** |
| * <p> |
| * An internal helper class for a atomic updates of an {@link InMemoryNodeModel}. |
| * </p> |
| * <p> |
| * This class performs updates on the node structure of a node model consisting of {@link ImmutableNode} objects. |
| * Because the nodes themselves cannot be changed updates are achieved by replacing parts of the structure with new |
| * nodes; the new nodes are copies of original nodes with the corresponding manipulations applied. Therefore, each |
| * update of a node in the structure results in a new structure in which the affected node is replaced by a new one, and |
| * this change bubbles up to the root node (because all parent nodes have to be replaced by instances with an updated |
| * child reference). |
| * </p> |
| * <p> |
| * A single update of a model may consist of multiple changes on nodes. For instance, a remove property operation can |
| * include many nodes. There are some reasons why such updates should be handled in a single "transaction" rather than |
| * executing them on altered node structures one by one: |
| * <ul> |
| * <li>An operation is typically executed on a set of source nodes from the original node hierarchy. While manipulating |
| * nodes, nodes of this set may be replaced by new ones. The handling of these replacements complicates things a |
| * lot.</li> |
| * <li>Performing all updates one after the other may cause more updates of nodes than necessary. Nodes near to the root |
| * node always have to be replaced when a child of them gets manipulated. If all these updates are deferred and handled |
| * in a single transaction, the resulting operation is more efficient.</li> |
| * </ul> |
| * </p> |
| */ |
| final class ModelTransaction { |
| |
| /** |
| * A specialized operation class for adding an attribute to a target node. |
| */ |
| private static final class AddAttributeOperation extends Operation { |
| /** The attribute name. */ |
| private final String attributeName; |
| |
| /** The attribute value. */ |
| private final Object attributeValue; |
| |
| /** |
| * Creates a new instance of {@code AddAttributeOperation}. |
| * |
| * @param name the name of the attribute |
| * @param value the value of the attribute |
| */ |
| public AddAttributeOperation(final String name, final Object value) { |
| attributeName = name; |
| attributeValue = value; |
| } |
| |
| @Override |
| protected ImmutableNode apply(final ImmutableNode target, final Operations operations) { |
| return target.setAttribute(attributeName, attributeValue); |
| } |
| } |
| |
| /** |
| * A specialized operation class for adding multiple attributes to a target node. |
| */ |
| private static final class AddAttributesOperation extends Operation { |
| /** The map with attributes. */ |
| private final Map<String, Object> attributes; |
| |
| /** |
| * Creates a new instance of {@code AddAttributesOperation}. |
| * |
| * @param attrs the map with attributes |
| */ |
| public AddAttributesOperation(final Map<String, Object> attrs) { |
| attributes = attrs; |
| } |
| |
| @Override |
| protected ImmutableNode apply(final ImmutableNode target, final Operations operations) { |
| return target.setAttributes(attributes); |
| } |
| } |
| |
| /** |
| * A specialized operation class which changes the name of a node. |
| */ |
| private static final class ChangeNodeNameOperation extends Operation { |
| /** The new node name. */ |
| private final String newName; |
| |
| /** |
| * Creates a new instance of {@code ChangeNodeNameOperation} and sets the new node name. |
| * |
| * @param name the new node name |
| */ |
| public ChangeNodeNameOperation(final String name) { |
| newName = name; |
| } |
| |
| @Override |
| protected ImmutableNode apply(final ImmutableNode target, final Operations operations) { |
| return target.setName(newName); |
| } |
| } |
| |
| /** |
| * A specialized operation class which changes the value of a node. |
| */ |
| private static final class ChangeNodeValueOperation extends Operation { |
| /** The new value for the affected node. */ |
| private final Object newValue; |
| |
| /** |
| * Creates a new instance of {@code ChangeNodeValueOperation} and initializes it with the new value to set for the node. |
| * |
| * @param value the new node value |
| */ |
| public ChangeNodeValueOperation(final Object value) { |
| newValue = value; |
| } |
| |
| @Override |
| protected ImmutableNode apply(final ImmutableNode target, final Operations operations) { |
| return target.setValue(newValue); |
| } |
| } |
| |
| /** |
| * A specialized {@code Operation} implementation for replacing the children of a target node. All other properties are |
| * not touched. With this operation single children of a node can be altered or removed; new children can be added. This |
| * operation is frequently used because each update of a node causes updates of the children of all parent nodes. |
| * Therefore, it is treated in a special way and allows adding further sub operations dynamically. |
| */ |
| private final class ChildrenUpdateOperation extends Operation { |
| /** A collection with new nodes to be added. */ |
| private Collection<ImmutableNode> newNodes; |
| |
| /** A collection with nodes to be removed. */ |
| private Set<ImmutableNode> nodesToRemove; |
| |
| /** |
| * A map with nodes to be replaced by others. The keys are the nodes to be replaced, the values the replacements. |
| */ |
| private Map<ImmutableNode, ImmutableNode> nodesToReplace; |
| |
| /** |
| * Adds a node to be added to the target of the operation. |
| * |
| * @param node the new node to be added |
| */ |
| public void addNewNode(final ImmutableNode node) { |
| newNodes = append(newNodes, node); |
| } |
| |
| /** |
| * Adds a collection of nodes to be added to the target of the operation. |
| * |
| * @param nodes the collection with new nodes |
| */ |
| public void addNewNodes(final Collection<? extends ImmutableNode> nodes) { |
| newNodes = concatenate(newNodes, nodes); |
| } |
| |
| /** |
| * Adds a node for a remove operation. This child node is going to be removed from its parent. |
| * |
| * @param node the child node to be removed |
| */ |
| public void addNodeToRemove(final ImmutableNode node) { |
| nodesToRemove = append(nodesToRemove, node); |
| } |
| |
| /** |
| * Adds a node for a replacement operation. The original node is going to be replaced by its replacement. |
| * |
| * @param org the original node |
| * @param replacement the replacement node |
| */ |
| public void addNodeToReplace(final ImmutableNode org, final ImmutableNode replacement) { |
| nodesToReplace = append(nodesToReplace, org, replacement); |
| } |
| |
| /** |
| * {@inheritDoc} This implementation applies changes on the children of the passed in target node according to its |
| * configuration: new nodes are added, replacements are performed, and nodes no longer needed are removed. |
| */ |
| @Override |
| protected ImmutableNode apply(final ImmutableNode target, final Operations operations) { |
| final Map<ImmutableNode, ImmutableNode> replacements = fetchReplacementMap(); |
| final Set<ImmutableNode> removals = fetchRemovalSet(); |
| final List<ImmutableNode> resultNodes = new LinkedList<>(); |
| |
| for (final ImmutableNode nd : target) { |
| final ImmutableNode repl = replacements.get(nd); |
| if (repl != null) { |
| resultNodes.add(repl); |
| replacedNodes.put(nd, repl); |
| } else if (removals.contains(nd)) { |
| removedNodes.add(nd); |
| } else { |
| resultNodes.add(nd); |
| } |
| } |
| |
| concatenate(resultNodes, newNodes); |
| operations.newNodesAdded(newNodes); |
| return target.replaceChildren(resultNodes); |
| } |
| |
| /** |
| * Adds all operations defined by the specified object to this instance. |
| * |
| * @param op the operation to be combined |
| */ |
| public void combine(final ChildrenUpdateOperation op) { |
| newNodes = concatenate(newNodes, op.newNodes); |
| nodesToReplace = concatenate(nodesToReplace, op.nodesToReplace); |
| nodesToRemove = concatenate(nodesToRemove, op.nodesToRemove); |
| } |
| |
| /** |
| * Returns a set with nodes to be removed. If no remove operations are pending, an empty set is returned. |
| * |
| * @return the set with nodes to be removed |
| */ |
| private Set<ImmutableNode> fetchRemovalSet() { |
| return nodesToRemove != null ? nodesToRemove : Collections.<ImmutableNode>emptySet(); |
| } |
| |
| /** |
| * Obtains the map with replacement nodes. If no replacements are defined, an empty map is returned. |
| * |
| * @return the map with replacement nodes |
| */ |
| private Map<ImmutableNode, ImmutableNode> fetchReplacementMap() { |
| return nodesToReplace != null ? nodesToReplace : Collections.<ImmutableNode, ImmutableNode>emptyMap(); |
| } |
| } |
| |
| /** |
| * An abstract base class representing an operation to be performed on a node. Concrete subclasses implement specific |
| * update operations. |
| */ |
| private abstract static class Operation { |
| /** |
| * Executes this operation on the provided target node returning the result. |
| * |
| * @param target the target node for this operation |
| * @param operations the current {@code Operations} instance |
| * @return the manipulated node |
| */ |
| protected abstract ImmutableNode apply(ImmutableNode target, Operations operations); |
| } |
| |
| /** |
| * A helper class which collects multiple update operations to be executed on a single node. |
| */ |
| private final class Operations { |
| /** An operation for manipulating child nodes. */ |
| private ChildrenUpdateOperation childrenOperation; |
| |
| /** |
| * A collection for the other operations to be performed on the target node. |
| */ |
| private Collection<Operation> operations; |
| |
| /** A collection with nodes added by an operation. */ |
| private Collection<ImmutableNode> addedNodesInOperation; |
| |
| /** |
| * Adds an operation which manipulates children. |
| * |
| * @param co the operation |
| */ |
| public void addChildrenOperation(final ChildrenUpdateOperation co) { |
| if (childrenOperation == null) { |
| childrenOperation = co; |
| } else { |
| childrenOperation.combine(co); |
| } |
| } |
| |
| /** |
| * Adds an operation. |
| * |
| * @param op the operation |
| */ |
| public void addOperation(final Operation op) { |
| operations = append(operations, op); |
| } |
| |
| /** |
| * Executes all operations stored in this object on the given target node. The resulting node then has to be integrated |
| * in the current node hierarchy. Unless the root node is already reached, this causes another updated operation to be |
| * created which replaces the manipulated child in the parent node. |
| * |
| * @param target the target node for this operation |
| * @param level the level of the target node |
| */ |
| public void apply(final ImmutableNode target, final int level) { |
| ImmutableNode node = target; |
| if (childrenOperation != null) { |
| node = childrenOperation.apply(node, this); |
| } |
| |
| if (operations != null) { |
| for (final Operation op : operations) { |
| node = op.apply(node, this); |
| } |
| } |
| |
| handleAddedNodes(node); |
| if (level == 0) { |
| // reached the root node |
| newRoot = node; |
| replacedNodes.put(target, node); |
| } else { |
| // propagate change |
| propagateChange(target, node, level); |
| } |
| } |
| |
| /** |
| * Checks whether new nodes have been added during operation execution. If so, the parent mapping has to be updated. |
| * |
| * @param node the resulting node after applying all operations |
| */ |
| private void handleAddedNodes(final ImmutableNode node) { |
| if (addedNodesInOperation != null) { |
| addedNodesInOperation.forEach(child -> { |
| parentMapping.put(child, node); |
| addedNodes.add(child); |
| }); |
| } |
| } |
| |
| /** |
| * Notifies this object that new nodes have been added by a sub operation. It has to be ensured that these nodes are |
| * added to the parent mapping. |
| * |
| * @param newNodes the collection of newly added nodes |
| */ |
| public void newNodesAdded(final Collection<ImmutableNode> newNodes) { |
| addedNodesInOperation = concatenate(addedNodesInOperation, newNodes); |
| } |
| |
| /** |
| * Propagates the changes on the target node to the next level above of the hierarchy. If the updated node is no longer |
| * defined, it can even be removed from its parent. Otherwise, it is just replaced. |
| * |
| * @param target the target node for this operation |
| * @param node the resulting node after applying all operations |
| * @param level the level of the target node |
| */ |
| private void propagateChange(final ImmutableNode target, final ImmutableNode node, final int level) { |
| final ImmutableNode parent = getParent(target); |
| final ChildrenUpdateOperation co = new ChildrenUpdateOperation(); |
| if (InMemoryNodeModel.checkIfNodeDefined(node)) { |
| co.addNodeToReplace(target, node); |
| } else { |
| co.addNodeToRemove(target); |
| } |
| fetchOperations(parent, level - 1).addChildrenOperation(co); |
| } |
| } |
| |
| /** |
| * A specialized operation class for removing an attribute from a target node. |
| */ |
| private static final class RemoveAttributeOperation extends Operation { |
| /** The attribute name. */ |
| private final String attributeName; |
| |
| /** |
| * Creates a new instance of {@code RemoveAttributeOperation}. |
| * |
| * @param name the name of the attribute |
| */ |
| public RemoveAttributeOperation(final String name) { |
| attributeName = name; |
| } |
| |
| @Override |
| protected ImmutableNode apply(final ImmutableNode target, final Operations operations) { |
| return target.removeAttribute(attributeName); |
| } |
| } |
| |
| /** |
| * Constant for the maximum number of entries in the replacement mapping. If this number is exceeded, the parent mapping |
| * is reconstructed. The number is a bit arbitrary. If it is too low, updates - especially on large node structures - |
| * are expensive because the parent mapping is often rebuild. If it is too big, read access to the model is slowed down |
| * because looking up the parent of a node is more complicated. |
| */ |
| private static final int MAX_REPLACEMENTS = 200; |
| |
| /** Constant for an unknown level. */ |
| private static final int LEVEL_UNKNOWN = -1; |
| |
| /** |
| * Appends a single element to a collection. The collection may be null, then it is created. |
| * |
| * @param col the collection |
| * @param node the element to be added |
| * @param <E> the type of elements involved |
| * @return the resulting collection |
| */ |
| private static <E> Collection<E> append(final Collection<E> col, final E node) { |
| final Collection<E> result = col != null ? col : new LinkedList<>(); |
| result.add(node); |
| return result; |
| } |
| |
| /** |
| * Adds a single key-value pair to a map. The map may be null, then it is created. |
| * |
| * @param map the map |
| * @param key the key |
| * @param value the value |
| * @param <K> the type of the key |
| * @param <V> the type of the value |
| * @return the resulting map |
| */ |
| private static <K, V> Map<K, V> append(final Map<K, V> map, final K key, final V value) { |
| final Map<K, V> result = map != null ? map : new HashMap<>(); |
| result.put(key, value); |
| return result; |
| } |
| |
| /** |
| * Appends a single element to a set. The set may be null then it is created. |
| * |
| * @param col the set |
| * @param elem the element to be added |
| * @param <E> the type of the elements involved |
| * @return the resulting set |
| */ |
| private static <E> Set<E> append(final Set<E> col, final E elem) { |
| final Set<E> result = col != null ? col : new HashSet<>(); |
| result.add(elem); |
| return result; |
| } |
| |
| /** |
| * Constructs the concatenation of two collections. Both can be null. |
| * |
| * @param col1 the first collection |
| * @param col2 the second collection |
| * @param <E> the type of the elements involved |
| * @return the resulting collection |
| */ |
| private static <E> Collection<E> concatenate(final Collection<E> col1, final Collection<? extends E> col2) { |
| if (col2 == null) { |
| return col1; |
| } |
| |
| final Collection<E> result = col1 != null ? col1 : new ArrayList<>(col2.size()); |
| result.addAll(col2); |
| return result; |
| } |
| |
| /** |
| * Constructs the concatenation of two maps. Both can be null. |
| * |
| * @param map1 the first map |
| * @param map2 the second map |
| * @param <K> the type of the keys |
| * @param <V> the type of the values |
| * @return the resulting map |
| */ |
| private static <K, V> Map<K, V> concatenate(final Map<K, V> map1, final Map<? extends K, ? extends V> map2) { |
| if (map2 == null) { |
| return map1; |
| } |
| |
| final Map<K, V> result = map1 != null ? map1 : new HashMap<>(); |
| result.putAll(map2); |
| return result; |
| } |
| |
| /** |
| * Constructs the concatenation of two sets. Both can be null. |
| * |
| * @param set1 the first set |
| * @param set2 the second set |
| * @param <E> the type of the elements involved |
| * @return the resulting set |
| */ |
| private static <E> Set<E> concatenate(final Set<E> set1, final Set<? extends E> set2) { |
| if (set2 == null) { |
| return set1; |
| } |
| |
| final Set<E> result = set1 != null ? set1 : new HashSet<>(); |
| result.addAll(set2); |
| return result; |
| } |
| |
| /** Stores the current tree data of the calling node model. */ |
| private final TreeData currentData; |
| |
| /** The root node for query operations. */ |
| private final ImmutableNode queryRoot; |
| |
| /** The selector to the root node of this transaction. */ |
| private final NodeSelector rootNodeSelector; |
| |
| /** The {@code NodeKeyResolver} to be used for this transaction. */ |
| private final NodeKeyResolver<ImmutableNode> resolver; |
| |
| /** A new replacement mapping. */ |
| private final Map<ImmutableNode, ImmutableNode> replacementMapping; |
| |
| /** The nodes replaced in this transaction. */ |
| private final Map<ImmutableNode, ImmutableNode> replacedNodes; |
| |
| /** A new parent mapping. */ |
| private final Map<ImmutableNode, ImmutableNode> parentMapping; |
| |
| /** A collection with nodes which have been added. */ |
| private final Collection<ImmutableNode> addedNodes; |
| |
| /** A collection with nodes which have been removed. */ |
| private final Collection<ImmutableNode> removedNodes; |
| |
| /** |
| * Stores all nodes which have been removed in this transaction (not only the root nodes of removed trees). |
| */ |
| private final Collection<ImmutableNode> allRemovedNodes; |
| |
| /** |
| * Stores the operations to be executed during this transaction. The map is sorted by the levels of the nodes to be |
| * manipulated: Operations on nodes down in the hierarchy are executed first because they affect the nodes closer to the |
| * root. |
| */ |
| private final SortedMap<Integer, Map<ImmutableNode, Operations>> operations; |
| |
| /** A map with reference objects to be added during this transaction. */ |
| private Map<ImmutableNode, Object> newReferences; |
| |
| /** The new root node. */ |
| private ImmutableNode newRoot; |
| |
| /** |
| * Creates a new instance of {@code ModelTransaction} for the current tree data. |
| * |
| * @param treeData the current {@code TreeData} structure to operate on |
| * @param selector an optional {@code NodeSelector} defining the target root node for this transaction; this can be used |
| * to perform operations on tracked nodes |
| * @param resolver the {@code NodeKeyResolver} |
| */ |
| public ModelTransaction(final TreeData treeData, final NodeSelector selector, final NodeKeyResolver<ImmutableNode> resolver) { |
| currentData = treeData; |
| this.resolver = resolver; |
| replacementMapping = getCurrentData().copyReplacementMapping(); |
| replacedNodes = new HashMap<>(); |
| parentMapping = getCurrentData().copyParentMapping(); |
| operations = new TreeMap<>(); |
| addedNodes = new LinkedList<>(); |
| removedNodes = new LinkedList<>(); |
| allRemovedNodes = new LinkedList<>(); |
| queryRoot = initQueryRoot(treeData, selector); |
| rootNodeSelector = selector; |
| } |
| |
| /** |
| * Adds an operation for adding a new child to a given parent node. |
| * |
| * @param parent the parent node |
| * @param newChild the new child to be added |
| */ |
| public void addAddNodeOperation(final ImmutableNode parent, final ImmutableNode newChild) { |
| final ChildrenUpdateOperation op = new ChildrenUpdateOperation(); |
| op.addNewNode(newChild); |
| fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op); |
| } |
| |
| /** |
| * Adds an operation for adding a number of new children to a given parent node. |
| * |
| * @param parent the parent node |
| * @param newNodes the collection of new child nodes |
| */ |
| public void addAddNodesOperation(final ImmutableNode parent, final Collection<? extends ImmutableNode> newNodes) { |
| final ChildrenUpdateOperation op = new ChildrenUpdateOperation(); |
| op.addNewNodes(newNodes); |
| fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op); |
| } |
| |
| /** |
| * Adds an operation for adding an attribute to a target node. |
| * |
| * @param target the target node |
| * @param name the name of the attribute |
| * @param value the value of the attribute |
| */ |
| public void addAttributeOperation(final ImmutableNode target, final String name, final Object value) { |
| fetchOperations(target, LEVEL_UNKNOWN).addOperation(new AddAttributeOperation(name, value)); |
| } |
| |
| /** |
| * Adds an operation for adding multiple attributes to a target node. |
| * |
| * @param target the target node |
| * @param attributes the map with attributes to be set |
| */ |
| public void addAttributesOperation(final ImmutableNode target, final Map<String, Object> attributes) { |
| fetchOperations(target, LEVEL_UNKNOWN).addOperation(new AddAttributesOperation(attributes)); |
| } |
| |
| /** |
| * Adds an operation for changing the name of a target node. |
| * |
| * @param target the target node |
| * @param newName the new name for this node |
| */ |
| public void addChangeNodeNameOperation(final ImmutableNode target, final String newName) { |
| fetchOperations(target, LEVEL_UNKNOWN).addOperation(new ChangeNodeNameOperation(newName)); |
| } |
| |
| /** |
| * Adds an operation for changing the value of a target node. |
| * |
| * @param target the target node |
| * @param newValue the new value for this node |
| */ |
| public void addChangeNodeValueOperation(final ImmutableNode target, final Object newValue) { |
| fetchOperations(target, LEVEL_UNKNOWN).addOperation(new ChangeNodeValueOperation(newValue)); |
| } |
| |
| /** |
| * Adds an operation for clearing the value of a target node. |
| * |
| * @param target the target node |
| */ |
| public void addClearNodeValueOperation(final ImmutableNode target) { |
| addChangeNodeValueOperation(target, null); |
| } |
| |
| /** |
| * Adds a new reference object for the given node. |
| * |
| * @param node the affected node |
| * @param ref the reference object for this node |
| */ |
| public void addNewReference(final ImmutableNode node, final Object ref) { |
| fetchReferenceMap().put(node, ref); |
| } |
| |
| /** |
| * Adds a map with new reference objects. The entries in this map are passed to the {@code ReferenceTracker} during |
| * execution of this transaction. |
| * |
| * @param refs the map with new reference objects |
| */ |
| public void addNewReferences(final Map<ImmutableNode, ?> refs) { |
| fetchReferenceMap().putAll(refs); |
| } |
| |
| /** |
| * Adds an operation for removing an attribute from a target node. |
| * |
| * @param target the target node |
| * @param name the name of the attribute |
| */ |
| public void addRemoveAttributeOperation(final ImmutableNode target, final String name) { |
| fetchOperations(target, LEVEL_UNKNOWN).addOperation(new RemoveAttributeOperation(name)); |
| } |
| |
| /** |
| * Adds an operation for removing a child node of a given node. |
| * |
| * @param parent the parent node |
| * @param node the child node to be removed |
| */ |
| public void addRemoveNodeOperation(final ImmutableNode parent, final ImmutableNode node) { |
| final ChildrenUpdateOperation op = new ChildrenUpdateOperation(); |
| op.addNodeToRemove(node); |
| fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op); |
| } |
| |
| /** |
| * Executes this transaction resulting in a new {@code TreeData} object. The object returned by this method serves as |
| * the definition of a new node structure for the calling model. |
| * |
| * @return the updated {@code TreeData} |
| */ |
| public TreeData execute() { |
| executeOperations(); |
| updateParentMapping(); |
| return new TreeData(newRoot, parentMapping, replacementMapping, |
| currentData.getNodeTracker().update(newRoot, rootNodeSelector, getResolver(), getCurrentData()), updateReferenceTracker()); |
| } |
| |
| /** |
| * Executes all operations in this transaction. |
| */ |
| private void executeOperations() { |
| while (!operations.isEmpty()) { |
| final Integer level = operations.lastKey(); // start down in hierarchy |
| operations.remove(level).forEach((k, v) -> v.apply(k, level)); |
| } |
| } |
| |
| /** |
| * Obtains the {@code Operations} object for manipulating the specified node. If no such object exists yet, it is |
| * created. The level can be undefined, then it is determined based on the target node. |
| * |
| * @param target the target node |
| * @param level the level of the target node (may be undefined) |
| * @return the {@code Operations} object for this node |
| */ |
| Operations fetchOperations(final ImmutableNode target, final int level) { |
| final Integer nodeLevel = Integer.valueOf(level == LEVEL_UNKNOWN ? level(target) : level); |
| final Map<ImmutableNode, Operations> levelOperations = operations.computeIfAbsent(nodeLevel, k -> new HashMap<>()); |
| return levelOperations.computeIfAbsent(target, k -> new Operations()); |
| } |
| |
| /** |
| * Returns the map with new reference objects. It is created if necessary. |
| * |
| * @return the map with reference objects |
| */ |
| private Map<ImmutableNode, Object> fetchReferenceMap() { |
| if (newReferences == null) { |
| newReferences = new HashMap<>(); |
| } |
| return newReferences; |
| } |
| |
| /** |
| * Gets the current {@code TreeData} object this transaction operates on. |
| * |
| * @return the associated {@code TreeData} object |
| */ |
| public TreeData getCurrentData() { |
| return currentData; |
| } |
| |
| /** |
| * Gets the parent node of the given node. |
| * |
| * @param node the node in question |
| * @return the parent of this node |
| */ |
| ImmutableNode getParent(final ImmutableNode node) { |
| return getCurrentData().getParent(node); |
| } |
| |
| /** |
| * Gets the root node to be used within queries. This is not necessarily the current root node of the model. If the |
| * operation is executed on a tracked node, this node has to be passed as root nodes to the expression engine. |
| * |
| * @return the root node for queries and calls to the expression engine |
| */ |
| public ImmutableNode getQueryRoot() { |
| return queryRoot; |
| } |
| |
| /** |
| * Gets the {@code NodeKeyResolver} used by this transaction. |
| * |
| * @return the {@code NodeKeyResolver} |
| */ |
| public NodeKeyResolver<ImmutableNode> getResolver() { |
| return resolver; |
| } |
| |
| /** |
| * Initializes the root node to be used within queries. If a tracked node selector is provided, this node becomes the |
| * root node. Otherwise, the actual root node is used. |
| * |
| * @param treeData the current data of the model |
| * @param selector an optional {@code NodeSelector} defining the target root |
| * @return the query root node for this transaction |
| */ |
| private ImmutableNode initQueryRoot(final TreeData treeData, final NodeSelector selector) { |
| return selector == null ? treeData.getRootNode() : treeData.getNodeTracker().getTrackedNode(selector); |
| } |
| |
| /** |
| * Determines the level of the specified node in the current hierarchy. The level of the root node is 0, the children of |
| * the root have level 1 and so on. |
| * |
| * @param node the node in question |
| * @return the level of this node |
| */ |
| private int level(final ImmutableNode node) { |
| ImmutableNode current = getCurrentData().getParent(node); |
| int level = 0; |
| while (current != null) { |
| level++; |
| current = getCurrentData().getParent(current); |
| } |
| return level; |
| } |
| |
| /** |
| * Rebuilds the parent mapping from scratch. This method is called if the replacement mapping exceeds its maximum size. |
| * In this case, it is cleared, and a new parent mapping is constructed for the new root node. |
| */ |
| private void rebuildParentMapping() { |
| replacementMapping.clear(); |
| parentMapping.clear(); |
| InMemoryNodeModel.updateParentMapping(parentMapping, newRoot); |
| } |
| |
| /** |
| * Removes the specified node completely from the replacement mapping. This also includes the nodes that replace the |
| * given one. |
| * |
| * @param node the node to be removed |
| */ |
| private void removeNodeFromReplacementMapping(final ImmutableNode node) { |
| ImmutableNode replacement = node; |
| do { |
| replacement = replacementMapping.remove(replacement); |
| } while (replacement != null); |
| } |
| |
| /** |
| * Removes a node and its children (recursively) from the parent and the replacement mappings. |
| * |
| * @param root the root of the subtree to be removed |
| */ |
| private void removeNodesFromParentAndReplacementMapping(final ImmutableNode root) { |
| NodeTreeWalker.INSTANCE.walkBFS(root, new ConfigurationNodeVisitorAdapter<ImmutableNode>() { |
| @Override |
| public void visitBeforeChildren(final ImmutableNode node, final NodeHandler<ImmutableNode> handler) { |
| allRemovedNodes.add(node); |
| parentMapping.remove(node); |
| removeNodeFromReplacementMapping(node); |
| } |
| }, getCurrentData()); |
| } |
| |
| /** |
| * Updates the parent mapping for the resulting {@code TreeData} instance. This method is called after all update |
| * operations have been executed. It ensures that the parent mapping is updated for the changes on the nodes structure. |
| */ |
| private void updateParentMapping() { |
| replacementMapping.putAll(replacedNodes); |
| if (replacementMapping.size() > MAX_REPLACEMENTS) { |
| rebuildParentMapping(); |
| } else { |
| updateParentMappingForAddedNodes(); |
| updateParentMappingForRemovedNodes(); |
| } |
| } |
| |
| /** |
| * Adds newly added nodes and their children to the parent mapping. |
| */ |
| private void updateParentMappingForAddedNodes() { |
| addedNodes.forEach(node -> InMemoryNodeModel.updateParentMapping(parentMapping, node)); |
| } |
| |
| /** |
| * Removes nodes that have been removed during this transaction from the parent and replacement mappings. |
| */ |
| private void updateParentMappingForRemovedNodes() { |
| removedNodes.forEach(this::removeNodesFromParentAndReplacementMapping); |
| } |
| |
| /** |
| * Returns an updated {@code ReferenceTracker} instance. The changes performed during this transaction are applied to |
| * the tracker. |
| * |
| * @return the updated tracker instance |
| */ |
| private ReferenceTracker updateReferenceTracker() { |
| ReferenceTracker tracker = currentData.getReferenceTracker(); |
| if (newReferences != null) { |
| tracker = tracker.addReferences(newReferences); |
| } |
| return tracker.updateReferences(replacedNodes, allRemovedNodes); |
| } |
| } |