blob: 9548f3809c35e714ccbbfc764cd9a96a73e8e3cb [file] [log] [blame]
/*
* 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 internally used helper class for an atomic update 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>
*
*/
class ModelTransaction
{
/**
* 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;
/** 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;
}
/**
* Returns the {@code NodeKeyResolver} used by this transaction.
*
* @return the {@code NodeKeyResolver}
*/
public NodeKeyResolver<ImmutableNode> getResolver()
{
return resolver;
}
/**
* Returns 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;
}
/**
* 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 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 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 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);
}
/**
* 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 clearing the value of a target node.
*
* @param target the target node
*/
public void addClearNodeValueOperation(final ImmutableNode target)
{
addChangeNodeValueOperation(target, null);
}
/**
* 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 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 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 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);
}
/**
* 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()
);
}
/**
* Returns the current {@code TreeData} object this transaction operates on.
* @return the associated {@code TreeData} object
*/
public TreeData getCurrentData()
{
return currentData;
}
/**
* Returns 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);
}
/**
* 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<>());
Operations ops = levelOperations.get(target);
if (ops == null)
{
ops = new Operations();
levelOperations.put(target, ops);
}
return ops;
}
/**
* 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;
}
/**
* Executes all operations in this transaction.
*/
private void executeOperations()
{
while (!operations.isEmpty())
{
final Integer level = operations.lastKey(); // start down in hierarchy
final Map<ImmutableNode, Operations> levelOps = operations.remove(level);
for (final Map.Entry<ImmutableNode, Operations> e : levelOps.entrySet())
{
e.getValue().apply(e.getKey(), level);
}
}
}
/**
* 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();
}
}
/**
* 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);
}
/**
* Adds newly added nodes and their children to the parent mapping.
*/
private void updateParentMappingForAddedNodes()
{
for (final ImmutableNode node : addedNodes)
{
InMemoryNodeModel.updateParentMapping(parentMapping, node);
}
}
/**
* Removes nodes that have been removed during this transaction from the
* parent and replacement mappings.
*/
private void updateParentMappingForRemovedNodes()
{
for (final ImmutableNode node : removedNodes)
{
removeNodesFromParentAndReplacementMapping(node);
}
}
/**
* 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());
}
/**
* 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);
}
/**
* 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);
}
/**
* 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;
}
/**
* 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 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;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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;
}
/**
* An abstract base class representing an operation to be performed on a
* node. Concrete subclasses implement specific update operations.
*/
private abstract 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 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 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 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);
}
/**
* 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 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);
}
/**
* 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);
}
/**
* {@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);
}
/**
* 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();
}
/**
* 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();
}
}
/**
* A specialized operation class for adding an attribute to a target node.
*/
private 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 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 for removing an attribute from a target
* node.
*/
private 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);
}
}
/**
* A specialized operation class which changes the value of a node.
*/
private 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 operation class which changes the name of a node.
*/
private 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 helper class which collects multiple update operations to be executed
* on a single node.
*/
private 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);
}
/**
* 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);
}
/**
* 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);
}
}
/**
* 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);
}
/**
* 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)
{
for (final ImmutableNode child : addedNodesInOperation)
{
parentMapping.put(child, node);
addedNodes.add(child);
}
}
}
}
}