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

org.apache.commons.configuration2.tree.InMemoryNodeModel 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.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.lang3.mutable.Mutable;
import org.apache.commons.lang3.mutable.MutableObject;

/**
 * 

* A specialized node model implementation which operates on {@link ImmutableNode} structures. *

*

* This {@code NodeModel} implementation keeps all its data as a tree of {@link ImmutableNode} objects in memory. The * managed structure can be manipulated in a thread-safe, non-blocking way. This is achieved by using atomic variables: * The root of the tree is stored in an atomic reference variable. Each update operation causes a new structure to be * constructed (which reuses as much from the original structure as possible). The old root node is then replaced by the * new one using an atomic compare-and-set operation. If this fails, the manipulation has to be done anew on the updated * structure. *

* * @since 2.0 */ public class InMemoryNodeModel implements NodeModel { /** * An interface used internally for handling concurrent updates. An implementation has to populate the passed in * {@code ModelTransaction}. The transaction is then executed, and an atomic update of the model's {@code TreeData} is * attempted. If this fails - because another update came across -, the whole operation has to be tried anew. */ private interface TransactionInitializer { /** * Initializes the specified transaction for an update operation. The return value indicates whether the transaction * should be executed. A result of false means that the update is to be aborted (maybe another update method was * called). * * @param tx the transaction to be initialized * @return a flag whether the update should continue */ boolean initTransaction(ModelTransaction tx); } /** * A dummy node handler instance used in operations which require only a limited functionality. */ private static final NodeHandler DUMMY_HANDLER = new TreeData(null, Collections.emptyMap(), Collections.emptyMap(), null, new ReferenceTracker()); /** * Handles an add property operation if the property to be added is an attribute. * * @param tx the transaction * @param addData the {@code NodeAddData} * @param values the collection with node values */ private static void addAttributeProperty(final ModelTransaction tx, final NodeAddData addData, final Iterable values) { if (addData.getPathNodes().isEmpty()) { tx.addAttributeOperation(addData.getParent(), addData.getNewNodeName(), values.iterator().next()); } else { final int pathNodeCount = addData.getPathNodes().size(); final ImmutableNode childWithAttribute = new ImmutableNode.Builder().name(addData.getPathNodes().get(pathNodeCount - 1)) .addAttribute(addData.getNewNodeName(), values.iterator().next()).create(); final ImmutableNode newChild = pathNodeCount > 1 ? createNodeOnPath(addData.getPathNodes().subList(0, pathNodeCount - 1).iterator(), Collections.singleton(childWithAttribute)) : childWithAttribute; tx.addAddNodeOperation(addData.getParent(), newChild); } } /** * Handles an add property operation if the property to be added is a node. * * @param tx the transaction * @param addData the {@code NodeAddData} * @param values the collection with node values */ private static void addNodeProperty(final ModelTransaction tx, final NodeAddData addData, final Iterable values) { final Collection newNodes = createNodesToAdd(addData.getNewNodeName(), values); addNodesByAddData(tx, addData, newNodes); } /** * Initializes a transaction to add a collection of nodes as described by a {@code NodeAddData} object. If necessary, * new path nodes are created. Eventually, the new nodes are added as children to the specified target node. * * @param tx the transaction * @param addData the {@code NodeAddData} * @param newNodes the collection of new child nodes */ private static void addNodesByAddData(final ModelTransaction tx, final NodeAddData addData, final Collection newNodes) { if (addData.getPathNodes().isEmpty()) { tx.addAddNodesOperation(addData.getParent(), newNodes); } else { final ImmutableNode newChild = createNodeToAddWithPath(addData, newNodes); tx.addAddNodeOperation(addData.getParent(), newChild); } } /** * Creates an exception referring to an invalid key for adding properties. Such an exception is thrown when an operation * tries to add something to an attribute. * * @param key the invalid key causing this exception * @return the exception */ private static IllegalArgumentException attributeKeyException(final String key) { return new IllegalArgumentException("New nodes cannot be added to an attribute key: " + key); } /** * Checks if the passed in node is defined. Result is true if the node contains any data. * * @param node the node in question * @return true if the node is defined, false otherwise */ static boolean checkIfNodeDefined(final ImmutableNode node) { return node.getValue() != null || !node.getChildren().isEmpty() || !node.getAttributes().isEmpty(); } /** * Creates a new data object with a tracked child node of the given parent node. If such a child node already exists, it * is used. Otherwise, a new one is created. * * @param current the current {@code TreeData} object * @param parent the parent node * @param childName the name of the child node * @param resolver the {@code NodeKeyResolver} * @param refSelector here the newly created {@code NodeSelector} is returned * @return the new {@code TreeData} instance */ private static TreeData createDataWithTrackedChildNode(final TreeData current, final ImmutableNode parent, final String childName, final NodeKeyResolver resolver, final MutableObject refSelector) { final TreeData newData; final List namedChildren = current.getChildren(parent, childName); if (!namedChildren.isEmpty()) { newData = updateDataWithNewTrackedNode(current, namedChildren.get(0), resolver, refSelector); } else { final ImmutableNode child = new ImmutableNode.Builder().name(childName).create(); final ModelTransaction tx = new ModelTransaction(current, null, resolver); tx.addAddNodeOperation(parent, child); newData = updateDataWithNewTrackedNode(tx.execute(), child, resolver, refSelector); } return newData; } /** * Recursive helper method for creating a path node for an add operation. All path nodes except for the last have a * single child. The last path node has the new nodes as children. * * @param it the iterator over the names of the path nodes * @param newNodes the collection of new child nodes * @return the newly created path node */ private static ImmutableNode createNodeOnPath(final Iterator it, final Collection newNodes) { final String nodeName = it.next(); final ImmutableNode.Builder builder; if (it.hasNext()) { builder = new ImmutableNode.Builder(1); builder.addChild(createNodeOnPath(it, newNodes)); } else { builder = new ImmutableNode.Builder(newNodes.size()); builder.addChildren(newNodes); } return builder.name(nodeName).create(); } /** * Creates a collection with new nodes with a given name and a value from a given collection. * * @param newNodeName the name of the new nodes * @param values the collection with node values * @return the newly created collection */ private static Collection createNodesToAdd(final String newNodeName, final Iterable values) { final Collection nodes = new LinkedList<>(); values.forEach(value -> nodes.add(new ImmutableNode.Builder().name(newNodeName).value(value).create())); return nodes; } /** * Creates a node structure consisting of the path nodes defined by the passed in {@code NodeAddData} instance and all * new child nodes. * * @param addData the {@code NodeAddData} * @param newNodes the collection of new child nodes * @return the parent node of the newly created hierarchy */ private static ImmutableNode createNodeToAddWithPath(final NodeAddData addData, final Collection newNodes) { return createNodeOnPath(addData.getPathNodes().iterator(), newNodes); } /** * Creates tracked node entries for the specified nodes and creates the corresponding selectors. * * @param refSelectors the reference where to store the selectors * @param nodes the nodes to be tracked * @param current the current {@code TreeData} object * @param resolver the {@code NodeKeyResolver} * @return the updated {@code TreeData} object */ private static TreeData createSelectorsForTrackedNodes(final Mutable> refSelectors, final List nodes, final TreeData current, final NodeKeyResolver resolver) { final List selectors = new ArrayList<>(nodes.size()); final Map cache = new HashMap<>(); nodes.forEach(node -> selectors.add(new NodeSelector(resolver.nodeKey(node, cache, current)))); refSelectors.setValue(selectors); final NodeTracker newTracker = current.getNodeTracker().trackNodes(selectors, nodes); return current.updateNodeTracker(newTracker); } /** * Determines the name of the root node for a merge operation. If a root name is provided, it is used. Otherwise, if the * current root node has no name, the name of the node to be merged is used. A result of null means that no node * name has to be set. * * @param rootNode the current root node * @param node the node to be merged with the root node * @param rootName the name of the resulting node * @return the new name of the root node */ private static String determineRootName(final ImmutableNode rootNode, final ImmutableNode node, final String rootName) { if (rootName != null) { return rootName; } if (rootNode.getNodeName() == null) { return node.getNodeName(); } return null; } /** * Initializes a transaction to clear the values of a property based on the passed in collection of affected results. * * @param tx the transaction to be initialized * @param results a collection with results pointing to the nodes to be cleared * @return a flag whether there are elements to be cleared */ private static boolean initializeClearTransaction(final ModelTransaction tx, final Collection> results) { results.forEach(result -> { if (result.isAttributeResult()) { tx.addRemoveAttributeOperation(result.getNode(), result.getAttributeName()); } else { tx.addClearNodeValueOperation(result.getNode()); } }); return !results.isEmpty(); } /** * Initializes a transaction to change the values of some query results based on the passed in map. * * @param tx the transaction to be initialized * @param changedValues the map defining the elements to be changed * @return a flag whether there are elements to be updated */ private static boolean initializeUpdateTransaction(final ModelTransaction tx, final Map, Object> changedValues) { changedValues.forEach((k, v) -> { final ImmutableNode node = k.getNode(); if (k.isAttributeResult()) { tx.addAttributeOperation(node, k.getAttributeName(), v); } else { tx.addChangeNodeValueOperation(node, v); } }); return !changedValues.isEmpty(); } /** * Determines the initial root node of this model. If a root node has been provided, it is used. Otherwise, an empty * dummy root node is created. * * @param providedRoot the passed in root node * @return the root node to be used */ private static ImmutableNode initialRootNode(final ImmutableNode providedRoot) { return providedRoot != null ? providedRoot : new ImmutableNode.Builder().create(); } /** * Adds a tracked node that has already been resolved to the specified data object. * * @param current the current {@code TreeData} object * @param node the node in question * @param resolver the {@code NodeKeyResolver} * @param refSelector here the newly created {@code NodeSelector} is returned * @return the new {@code TreeData} instance */ private static TreeData updateDataWithNewTrackedNode(final TreeData current, final ImmutableNode node, final NodeKeyResolver resolver, final MutableObject refSelector) { final NodeSelector selector = new NodeSelector(resolver.nodeKey(node, new HashMap<>(), current)); refSelector.setValue(selector); final NodeTracker newTracker = current.getNodeTracker().trackNodes(Collections.singleton(selector), Collections.singleton(node)); return current.updateNodeTracker(newTracker); } /** * Updates the mapping from nodes to their parents for the passed in hierarchy of nodes. This method traverses all * children and grand-children of the passed in root node. For each node in the subtree the parent relation is added to * the map. * * @param parents the map with parent nodes * @param root the root node of the current tree */ static void updateParentMapping(final Map parents, final ImmutableNode root) { NodeTreeWalker.INSTANCE.walkBFS(root, new ConfigurationNodeVisitorAdapter() { @Override public void visitBeforeChildren(final ImmutableNode node, final NodeHandler handler) { node.forEach(c -> parents.put(c, node)); } }, DUMMY_HANDLER); } /** * Checks whether the specified collection with values is not empty. * * @param values the collection with node values * @return true if values are provided, false otherwise */ private static boolean valuesNotEmpty(final Iterable values) { return values.iterator().hasNext(); } /** Stores information about the current nodes structure. */ private final AtomicReference structure; /** * Creates a new instance of {@code InMemoryNodeModel} which is initialized with an empty root node. */ public InMemoryNodeModel() { this(null); } /** * Creates a new instance of {@code InMemoryNodeModel} and initializes it from the given root node. If the passed in * node is null, a new, empty root node is created. * * @param root the new root node for this model */ public InMemoryNodeModel(final ImmutableNode root) { structure = new AtomicReference<>(createTreeData(initialRootNode(root), null)); } @Override public void addNodes(final String key, final Collection nodes, final NodeKeyResolver resolver) { addNodes(key, null, nodes, resolver); } /** * Adds new nodes using a tracked node as root node. This method works like the normal {@code addNodes()} method, but * the origin of the operation (also for the interpretation of the passed in key) is a tracked node identified by the * passed in {@code NodeSelector}. The selector can be null, then the root node is assumed. * * @param key the key * @param selector the {@code NodeSelector} defining the root node (or null) * @param nodes the collection of new nodes to be added * @param resolver the {@code NodeKeyResolver} * @throws ConfigurationRuntimeException if the selector cannot be resolved */ public void addNodes(final String key, final NodeSelector selector, final Collection nodes, final NodeKeyResolver resolver) { if (nodes != null && !nodes.isEmpty()) { updateModel(tx -> { final List> results = resolver.resolveKey(tx.getQueryRoot(), key, tx.getCurrentData()); if (results.size() == 1) { if (results.get(0).isAttributeResult()) { throw attributeKeyException(key); } tx.addAddNodesOperation(results.get(0).getNode(), nodes); } else { final NodeAddData addData = resolver.resolveAddKey(tx.getQueryRoot(), key, tx.getCurrentData()); if (addData.isAttribute()) { throw attributeKeyException(key); } final ImmutableNode newNode = new ImmutableNode.Builder(nodes.size()).name(addData.getNewNodeName()).addChildren(nodes).create(); addNodesByAddData(tx, addData, Collections.singleton(newNode)); } return true; }, selector, resolver); } } @Override public void addProperty(final String key, final Iterable values, final NodeKeyResolver resolver) { addProperty(key, null, values, resolver); } /** * Adds new property values using a tracked node as root node. This method works like the normal {@code addProperty()} * method, but the origin of the operation (also for the interpretation of the passed in key) is a tracked node * identified by the passed in {@code NodeSelector}. The selector can be null, then the root node is assumed. * * @param key the key * @param selector the {@code NodeSelector} defining the root node (or null) * @param values the values to be added * @param resolver the {@code NodeKeyResolver} * @throws ConfigurationRuntimeException if the selector cannot be resolved */ public void addProperty(final String key, final NodeSelector selector, final Iterable values, final NodeKeyResolver resolver) { if (valuesNotEmpty(values)) { updateModel(tx -> { initializeAddTransaction(tx, key, values, resolver); return true; }, selector, resolver); } } /** * {@inheritDoc} A new empty root node is created with the same name as the current root node. Implementation note: * Because this is a hard reset the usual dance for dealing with concurrent updates is not required here. * * @param resolver the {@code NodeKeyResolver} */ @Override public void clear(final NodeKeyResolver resolver) { final ImmutableNode newRoot = new ImmutableNode.Builder().name(getRootNode().getNodeName()).create(); setRootNode(newRoot); } /** * {@inheritDoc} If this operation leaves an affected node in an undefined state, it is removed from the model. */ @Override public void clearProperty(final String key, final NodeKeyResolver resolver) { clearProperty(key, null, resolver); } /** * Clears a property using a tracked node as root node. This method works like the normal {@code clearProperty()} * method, but the origin of the operation (also for the interpretation of the passed in key) is a tracked node * identified by the passed in {@code NodeSelector}. The selector can be null, then the root node is assumed. * * @param key the key * @param selector the {@code NodeSelector} defining the root node (or null) * @param resolver the {@code NodeKeyResolver} * @throws ConfigurationRuntimeException if the selector cannot be resolved */ public void clearProperty(final String key, final NodeSelector selector, final NodeKeyResolver resolver) { updateModel(tx -> { final List> results = resolver.resolveKey(tx.getQueryRoot(), key, tx.getCurrentData()); return initializeClearTransaction(tx, results); }, selector, resolver); } /** * {@inheritDoc} This implementation checks whether nodes become undefined after subtrees have been removed. If this is * the case, such nodes are removed, too. Return value is a collection with {@code QueryResult} objects for the elements * to be removed from the model. */ @Override public List> clearTree(final String key, final NodeKeyResolver resolver) { return clearTree(key, null, resolver); } /** * Clears a whole sub tree using a tracked node as root node. This method works like the normal {@code clearTree()} * method, but the origin of the operation (also for the interpretation of the passed in key) is a tracked node * identified by the passed in {@code NodeSelector}. The selector can be null, then the root node is assumed. * * @param key the key * @param selector the {@code NodeSelector} defining the root node (or null) * @param resolver the {@code NodeKeyResolver} * @return a list with the results to be removed * @throws ConfigurationRuntimeException if the selector cannot be resolved */ public List> clearTree(final String key, final NodeSelector selector, final NodeKeyResolver resolver) { final List> removedElements = new LinkedList<>(); updateModel(tx -> { boolean changes = false; final TreeData currentStructure = tx.getCurrentData(); final List> results = resolver.resolveKey(tx.getQueryRoot(), key, currentStructure); removedElements.clear(); removedElements.addAll(results); for (final QueryResult result : results) { if (result.isAttributeResult()) { tx.addRemoveAttributeOperation(result.getNode(), result.getAttributeName()); } else { if (result.getNode() == currentStructure.getRootNode()) { // the whole model is to be cleared clear(resolver); return false; } tx.addRemoveNodeOperation(currentStructure.getParent(result.getNode()), result.getNode()); } changes = true; } return changes; }, selector, resolver); return removedElements; } /** * Creates the mapping to parent nodes for the nodes structured represented by the passed in root node. Each node is * assigned its parent node. Here an iterative algorithm is used rather than a recursive one to avoid stack overflow for * huge structures. * * @param root the root node of the structure * @return the parent node mapping */ private Map createParentMapping(final ImmutableNode root) { final Map parents = new HashMap<>(); updateParentMapping(parents, root); return parents; } /** * Creates a {@code TreeData} object for the specified root node. * * @param root the root node of the current tree * @param current the current {@code TreeData} object (may be null) * @return the {@code TreeData} describing the current tree */ private TreeData createTreeData(final ImmutableNode root, final TreeData current) { final NodeTracker newTracker = current != null ? current.getNodeTracker().detachAllTrackedNodes() : new NodeTracker(); return createTreeDataForRootAndTracker(root, newTracker); } /** * Creates a {@code TreeData} object for the specified root node and {@code NodeTracker}. Other parameters are set to * default values. * * @param root the new root node for this model * @param newTracker the new {@code NodeTracker} * @return the new {@code TreeData} object */ private TreeData createTreeDataForRootAndTracker(final ImmutableNode root, final NodeTracker newTracker) { return new TreeData(root, createParentMapping(root), Collections.emptyMap(), newTracker, new ReferenceTracker()); } /** * Executes a transaction on the current data of this model. This method is called if an operation is to be executed on * the model's root node or a tracked node which is not yet detached. * * @param txInit the {@code TransactionInitializer} * @param selector an optional {@code NodeSelector} defining the target node * @param currentData the current data of the model * @param resolver the {@code NodeKeyResolver} * @return a flag whether the operation has been completed successfully */ private boolean executeTransactionOnCurrentStructure(final TransactionInitializer txInit, final NodeSelector selector, final TreeData currentData, final NodeKeyResolver resolver) { final boolean done; final ModelTransaction tx = new ModelTransaction(currentData, selector, resolver); if (!txInit.initTransaction(tx)) { done = true; } else { final TreeData newData = tx.execute(); done = structure.compareAndSet(tx.getCurrentData(), newData); } return done; } /** * Tries to execute a transaction on the model of a detached tracked node. This method checks whether the target node of * the transaction is a tracked node and if this node is already detached. If this is the case, the update operation is * independent on this model and has to be executed on the specific model for the detached node. * * @param txInit the {@code TransactionInitializer} * @param selector an optional {@code NodeSelector} defining the target node * @param currentData the current data of the model * @param resolver the {@code NodeKeyResolver} @return a flag whether the transaction could be executed * @throws ConfigurationRuntimeException if the selector cannot be resolved */ private boolean executeTransactionOnDetachedTrackedNode(final TransactionInitializer txInit, final NodeSelector selector, final TreeData currentData, final NodeKeyResolver resolver) { if (selector != null) { final InMemoryNodeModel detachedNodeModel = currentData.getNodeTracker().getDetachedNodeModel(selector); if (detachedNodeModel != null) { detachedNodeModel.updateModel(txInit, null, resolver); return true; } } return false; } /** * {@inheritDoc} This implementation simply returns the current root node of this model. */ @Override public ImmutableNode getInMemoryRepresentation() { return getTreeData().getRootNode(); } /** * {@inheritDoc} {@code InMemoryNodeModel} implements the {@code NodeHandler} interface itself. So this implementation * just returns the this reference. */ @Override public NodeHandler getNodeHandler() { return getReferenceNodeHandler(); } /** * Gets a {@code ReferenceNodeHandler} object for this model. This extended node handler can be used to query * references objects stored for this model. * * @return the {@code ReferenceNodeHandler} */ public ReferenceNodeHandler getReferenceNodeHandler() { return getTreeData(); } /** * Gets the root node of this mode. Note: This method should be used with care. The model may be updated concurrently * which causes the root node to be replaced. If the root node is to be processed further (e.g. by executing queries on * it), the model should be asked for its {@code NodeHandler}, and the root node should be obtained from there. The * connection between a node handler and its root node remain constant because an update of the model causes the whole * node handler to be replaced. * * @return the current root node */ public ImmutableNode getRootNode() { return getTreeData().getRootNode(); } /** * Gets the current {@code ImmutableNode} instance associated with the given {@code NodeSelector}. The node must be a * tracked node, i.e. {@link #trackNode(NodeSelector, NodeKeyResolver)} must have been called before with the given * selector. * * @param selector the {@code NodeSelector} defining the desired node * @return the current {@code ImmutableNode} associated with this selector * @throws ConfigurationRuntimeException if the selector is unknown */ public ImmutableNode getTrackedNode(final NodeSelector selector) { return structure.get().getNodeTracker().getTrackedNode(selector); } /** * Gets a {@code NodeHandler} for a tracked node. Such a handler may be required for operations on a sub tree of the * model. The handler to be returned depends on the current state of the tracked node. If it is still active, a handler * is used which shares some data (especially the parent mapping) with this model. Detached track nodes in contrast have * their own separate model; in this case a handler associated with this model is returned. * * @param selector the {@code NodeSelector} defining the tracked node * @return a {@code NodeHandler} for this tracked node * @throws ConfigurationRuntimeException if the selector is unknown */ public NodeHandler getTrackedNodeHandler(final NodeSelector selector) { final TreeData currentData = structure.get(); final InMemoryNodeModel detachedNodeModel = currentData.getNodeTracker().getDetachedNodeModel(selector); return detachedNodeModel != null ? detachedNodeModel.getNodeHandler() : new TrackedNodeHandler(currentData.getNodeTracker().getTrackedNode(selector), currentData); } /** * Gets the current {@code TreeData} object. This object contains all information about the current node structure. * * @return the current {@code TreeData} object */ TreeData getTreeData() { return structure.get(); } /** * Initializes a transaction for an add operation. * * @param tx the transaction to be initialized * @param key the key * @param values the collection with node values * @param resolver the {@code NodeKeyResolver} */ private void initializeAddTransaction(final ModelTransaction tx, final String key, final Iterable values, final NodeKeyResolver resolver) { final NodeAddData addData = resolver.resolveAddKey(tx.getQueryRoot(), key, tx.getCurrentData()); if (addData.isAttribute()) { addAttributeProperty(tx, addData, values); } else { addNodeProperty(tx, addData, values); } } /** * Returns a flag whether the specified tracked node is detached. As long as the {@code NodeSelector} associated with * that node returns a single instance, the tracked node is said to be life. If now an update of the model * happens which invalidates the selector (maybe the target node was removed), the tracked node becomes detached. It is * still possible to query the node; here the latest valid instance is returned. But further changes on the node model * are no longer tracked for this node. So even if there are further changes which would make the {@code NodeSelector} * valid again, the tracked node stays in detached state. * * @param selector the {@code NodeSelector} defining the desired node * @return a flag whether this tracked node is in detached state * @throws ConfigurationRuntimeException if the selector is unknown */ public boolean isTrackedNodeDetached(final NodeSelector selector) { return structure.get().getNodeTracker().isTrackedNodeDetached(selector); } /** * Merges the root node of this model with the specified node. This method is typically caused by configuration * implementations when a configuration source is loaded, and its data has to be added to the model. It is possible to * define the new name of the root node and to pass in a map with reference objects. * * @param node the node to be merged with the root node * @param rootName the new name of the root node; can be null, then the name of the root node is not changed * unless it is null * @param references an optional map with reference objects * @param rootRef an optional reference object for the new root node * @param resolver the {@code NodeKeyResolver} */ public void mergeRoot(final ImmutableNode node, final String rootName, final Map references, final Object rootRef, final NodeKeyResolver resolver) { updateModel(tx -> { final TreeData current = tx.getCurrentData(); final String newRootName = determineRootName(current.getRootNode(), node, rootName); if (newRootName != null) { tx.addChangeNodeNameOperation(current.getRootNode(), newRootName); } tx.addAddNodesOperation(current.getRootNode(), node.getChildren()); tx.addAttributesOperation(current.getRootNode(), node.getAttributes()); if (node.getValue() != null) { tx.addChangeNodeValueOperation(current.getRootNode(), node.getValue()); } if (references != null) { tx.addNewReferences(references); } if (rootRef != null) { tx.addNewReference(current.getRootNode(), rootRef); } return true; }, null, resolver); } /** * Replaces an active tracked node. The node then becomes detached. * * @param currentData the current data of the model * @param selector the {@code NodeSelector} defining the tracked node * @param newNode the node replacing the tracked node * @return a flag whether the operation was successful */ private boolean replaceActiveTrackedNode(final TreeData currentData, final NodeSelector selector, final ImmutableNode newNode) { final NodeTracker newTracker = currentData.getNodeTracker().replaceAndDetachTrackedNode(selector, newNode); return structure.compareAndSet(currentData, currentData.updateNodeTracker(newTracker)); } /** * Replaces a tracked node if it is already detached. * * @param currentData the current data of the model * @param selector the {@code NodeSelector} defining the tracked node * @param newNode the node replacing the tracked node * @return a flag whether the operation was successful */ private boolean replaceDetachedTrackedNode(final TreeData currentData, final NodeSelector selector, final ImmutableNode newNode) { final InMemoryNodeModel detachedNodeModel = currentData.getNodeTracker().getDetachedNodeModel(selector); if (detachedNodeModel != null) { detachedNodeModel.setRootNode(newNode); return true; } return false; } /** * Replaces the root node of this model. This method is similar to {@link #setRootNode(ImmutableNode)}; however, tracked * nodes will not get lost. The model applies the selectors of all tracked nodes on the new nodes hierarchy, so that * corresponding nodes are selected (this may cause nodes to become detached if a select operation fails). This * operation is useful if the new nodes hierarchy to be set is known to be similar to the old one. Note that reference * objects are lost; there is no way to automatically match nodes between the old and the new nodes hierarchy. * * @param newRoot the new root node to be set (must not be null) * @param resolver the {@code NodeKeyResolver} * @throws IllegalArgumentException if the new root node is null */ public void replaceRoot(final ImmutableNode newRoot, final NodeKeyResolver resolver) { if (newRoot == null) { throw new IllegalArgumentException("Replaced root node must not be null!"); } final TreeData current = structure.get(); // this step is needed to get a valid NodeHandler final TreeData temp = createTreeDataForRootAndTracker(newRoot, current.getNodeTracker()); structure.set(temp.updateNodeTracker(temp.getNodeTracker().update(newRoot, null, resolver, temp))); } /** * Replaces a tracked node by another node. If the tracked node is not yet detached, it becomes now detached. The passed * in node (which must not be null) becomes the new root node of an independent model for this tracked node. * Further updates of this model do not affect the tracked node's model and vice versa. * * @param selector the {@code NodeSelector} defining the tracked node * @param newNode the node replacing the tracked node (must not be null) * @throws ConfigurationRuntimeException if the selector cannot be resolved * @throws IllegalArgumentException if the replacement node is null */ public void replaceTrackedNode(final NodeSelector selector, final ImmutableNode newNode) { if (newNode == null) { throw new IllegalArgumentException("Replacement node must not be null!"); } boolean done; do { final TreeData currentData = structure.get(); done = replaceDetachedTrackedNode(currentData, selector, newNode) || replaceActiveTrackedNode(currentData, selector, newNode); } while (!done); } /** * Allows tracking all nodes selected by a key. This method evaluates the specified key on the current nodes structure. * For all selected nodes corresponding {@code NodeSelector} objects are created, and they are tracked. The returned * collection of {@code NodeSelector} objects can be used for interacting with the selected nodes. * * @param key the key for selecting the nodes to track * @param resolver the {@code NodeKeyResolver} * @return a collection with the {@code NodeSelector} objects for the new tracked nodes */ public Collection selectAndTrackNodes(final String key, final NodeKeyResolver resolver) { final Mutable> refSelectors = new MutableObject<>(); boolean done; do { final TreeData current = structure.get(); final List nodes = resolver.resolveNodeKey(current.getRootNode(), key, current); if (nodes.isEmpty()) { return Collections.emptyList(); } done = structure.compareAndSet(current, createSelectorsForTrackedNodes(refSelectors, nodes, current, resolver)); } while (!done); return refSelectors.getValue(); } /** * Sets the value of a property using a tracked node as root node. This method works like the normal * {@code setProperty()} method, but the origin of the operation (also for the interpretation of the passed in key) is a * tracked node identified by the passed in {@code NodeSelector}. The selector can be null, then the root node is * assumed. * * @param key the key * @param selector the {@code NodeSelector} defining the root node (or null) * @param value the new value for this property * @param resolver the {@code NodeKeyResolver} * @throws ConfigurationRuntimeException if the selector cannot be resolved */ public void setProperty(final String key, final NodeSelector selector, final Object value, final NodeKeyResolver resolver) { updateModel(tx -> { boolean added = false; final NodeUpdateData updateData = resolver.resolveUpdateKey(tx.getQueryRoot(), key, value, tx.getCurrentData()); if (!updateData.getNewValues().isEmpty()) { initializeAddTransaction(tx, key, updateData.getNewValues(), resolver); added = true; } final boolean cleared = initializeClearTransaction(tx, updateData.getRemovedNodes()); final boolean updated = initializeUpdateTransaction(tx, updateData.getChangedValues()); return added || cleared || updated; }, selector, resolver); } @Override public void setProperty(final String key, final Object value, final NodeKeyResolver resolver) { setProperty(key, null, value, resolver); } /** * {@inheritDoc} All tracked nodes and reference objects managed by this model are cleared.Care has to be taken when * this method is used and the model is accessed by multiple threads. It is not deterministic which concurrent * operations see the old root and which see the new root node. * * @param newRoot the new root node to be set (can be null, then an empty root node is set) */ @Override public void setRootNode(final ImmutableNode newRoot) { structure.set(createTreeData(initialRootNode(newRoot), structure.get())); } /** * Tracks all nodes which are children of the node selected by the passed in key. If the key selects exactly one node, * for all children of this node {@code NodeSelector} objects are created, and they become tracked nodes. The returned * collection of {@code NodeSelector} objects can be used for interacting with the selected nodes. * * @param key the key for selecting the parent node whose children are to be tracked * @param resolver the {@code NodeKeyResolver} * @return a collection with the {@code NodeSelector} objects for the new tracked nodes */ public Collection trackChildNodes(final String key, final NodeKeyResolver resolver) { final Mutable> refSelectors = new MutableObject<>(); boolean done; do { refSelectors.setValue(Collections.emptyList()); final TreeData current = structure.get(); final List nodes = resolver.resolveNodeKey(current.getRootNode(), key, current); if (nodes.size() == 1) { final ImmutableNode node = nodes.get(0); done = node.getChildren().isEmpty() || structure.compareAndSet(current, createSelectorsForTrackedNodes(refSelectors, node.getChildren(), current, resolver)); } else { done = true; } } while (!done); return refSelectors.getValue(); } /** * Tracks a node which is a child of another node selected by the passed in key. If the selected node has a child node * with this name, it is tracked and its selector is returned. Otherwise, a new child node with this name is created * first. * * @param key the key for selecting the parent node * @param childName the name of the child node * @param resolver the {@code NodeKeyResolver} * @return the {@code NodeSelector} for the tracked child node * @throws ConfigurationRuntimeException if the passed in key does not select a single node */ public NodeSelector trackChildNodeWithCreation(final String key, final String childName, final NodeKeyResolver resolver) { final MutableObject refSelector = new MutableObject<>(); boolean done; do { final TreeData current = structure.get(); final List nodes = resolver.resolveNodeKey(current.getRootNode(), key, current); if (nodes.size() != 1) { throw new ConfigurationRuntimeException("Key does not select a single node: " + key); } final ImmutableNode parent = nodes.get(0); final TreeData newData = createDataWithTrackedChildNode(current, parent, childName, resolver, refSelector); done = structure.compareAndSet(current, newData); } while (!done); return refSelector.getValue(); } /** * Adds a node to be tracked. After this method has been called with a specific {@code NodeSelector}, the node * associated with this key can be always obtained using {@link #getTrackedNode(NodeSelector)} with the same selector. * This is useful because during updates of a model parts of the structure are replaced. Therefore, it is not a good * idea to simply hold a reference to a node; this might become outdated soon. Rather, the node should be tracked. This * mechanism ensures that always the correct node reference can be obtained. * * @param selector the {@code NodeSelector} defining the desired node * @param resolver the {@code NodeKeyResolver} * @throws ConfigurationRuntimeException if the selector does not select a single node */ public void trackNode(final NodeSelector selector, final NodeKeyResolver resolver) { boolean done; do { final TreeData current = structure.get(); final NodeTracker newTracker = current.getNodeTracker().trackNode(current.getRootNode(), selector, resolver, current); done = structure.compareAndSet(current, current.updateNodeTracker(newTracker)); } while (!done); } /** * Removes a tracked node. This method is the opposite of {@code trackNode()}. It has to be called if there is no longer * the need to track a specific node. Note that for each call of {@code trackNode()} there has to be a corresponding * {@code untrackNode()} call. This ensures that multiple observers can track the same node. * * @param selector the {@code NodeSelector} defining the desired node * @throws ConfigurationRuntimeException if the specified node is not tracked */ public void untrackNode(final NodeSelector selector) { boolean done; do { final TreeData current = structure.get(); final NodeTracker newTracker = current.getNodeTracker().untrackNode(selector); done = structure.compareAndSet(current, current.updateNodeTracker(newTracker)); } while (!done); } /** * Performs a non-blocking, thread-safe update of this model based on a transaction initialized by the passed in * initializer. This method uses the atomic reference for the model's current data to ensure that an update was * successful even if the model is concurrently accessed. * * @param txInit the {@code TransactionInitializer} * @param selector an optional {@code NodeSelector} defining the target node of the transaction * @param resolver the {@code NodeKeyResolver} */ private void updateModel(final TransactionInitializer txInit, final NodeSelector selector, final NodeKeyResolver resolver) { boolean done; do { final TreeData currentData = getTreeData(); done = executeTransactionOnDetachedTrackedNode(txInit, selector, currentData, resolver) || executeTransactionOnCurrentStructure(txInit, selector, currentData, resolver); } while (!done); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy