All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.configuration2.tree.ModelTransaction Maven / Gradle / Ivy

Go to download

Tools to assist in the reading of configuration/preferences files in various formats

The newest version!
/*
 * 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;

/**
 * 

* An internal helper class for a atomic updates of an {@link InMemoryNodeModel}. *

*

* 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). *

*

* 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: *

    *
  • 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.
  • *
  • 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.
  • *
*

*/ 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 attributes; /** * Creates a new instance of {@code AddAttributesOperation}. * * @param attrs the map with attributes */ public AddAttributesOperation(final Map 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 newNodes; /** A collection with nodes to be removed. */ private Set nodesToRemove; /** * A map with nodes to be replaced by others. The keys are the nodes to be replaced, the values the replacements. */ private Map 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 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 replacements = fetchReplacementMap(); final Set removals = fetchRemovalSet(); final List 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 fetchRemovalSet() { return nodesToRemove != null ? nodesToRemove : Collections.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 fetchReplacementMap() { return nodesToReplace != null ? nodesToReplace : Collections.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 operations; /** A collection with nodes added by an operation. */ private Collection 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 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 the type of elements involved * @return the resulting collection */ private static Collection append(final Collection col, final E node) { final Collection 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 the type of the key * @param the type of the value * @return the resulting map */ private static Map append(final Map map, final K key, final V value) { final Map 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 the type of the elements involved * @return the resulting set */ private static Set append(final Set col, final E elem) { final Set 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 the type of the elements involved * @return the resulting collection */ private static Collection concatenate(final Collection col1, final Collection col2) { if (col2 == null) { return col1; } final Collection 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 the type of the keys * @param the type of the values * @return the resulting map */ private static Map concatenate(final Map map1, final Map map2) { if (map2 == null) { return map1; } final Map 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 the type of the elements involved * @return the resulting set */ private static Set concatenate(final Set set1, final Set set2) { if (set2 == null) { return set1; } final Set 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 resolver; /** A new replacement mapping. */ private final Map replacementMapping; /** The nodes replaced in this transaction. */ private final Map replacedNodes; /** A new parent mapping. */ private final Map parentMapping; /** A collection with nodes which have been added. */ private final Collection addedNodes; /** A collection with nodes which have been removed. */ private final Collection removedNodes; /** * Stores all nodes which have been removed in this transaction (not only the root nodes of removed trees). */ private final Collection 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> operations; /** A map with reference objects to be added during this transaction. */ private Map 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 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 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 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 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 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 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 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() { @Override public void visitBeforeChildren(final ImmutableNode node, final NodeHandler 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy