blob: a997f5912ecb542a838b3e72d1a9deb8977bdfb1 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* James Blackburn (Broadcom Corp.) - ongoing development
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
*******************************************************************************/
package org.eclipse.core.internal.watson;
import java.util.HashMap;
import org.eclipse.core.internal.dtree.*;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.StringPool;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
/**
* An ElementTree can be viewed as a generic rooted tree that stores
* a hierarchy of elements. An element in the tree consists of a
* (name, data, children) 3-tuple. The name can be any String, and
* the data can be any Object. The children are a collection of zero
* or more elements that logically fall below their parent in the tree.
* The implementation makes no guarantees about the ordering of children.
*
* Elements in the tree are referenced by a key that consists of the names
* of all elements on the path from the root to that element in the tree.
* For example, if root node "a" has child "b", which has child "c", element
* "c" can be referenced in the tree using the key (/a/b/c). Keys are represented
* using IPath objects, where the Paths are relative to the root element of the
* tree.
* @see IPath
*
* Each ElementTree has a single root element that is created implicitly and
* is always present in any tree. This root corresponds to the key (/),
* or the singleton <code>Path.ROOT</code>. The root element cannot be created
* or deleted, and its data and name cannot be set. The root element's children
* however can be modified (added, deleted, etc). The root path can be obtained
* using the <code>getRoot()</code> method.
*
* ElementTrees are modified in generations. The method <code>newEmptyDelta()</code>
* returns a new tree generation that can be modified arbitrarily by the user.
* For the purpose of explanation, we call such a tree "active".
* When the method <code>immutable()</code> is called, that tree generation is
* frozen, and can never again be modified. A tree must be immutable before
* a new tree generation can start. Since all ancestor trees are immutable,
* different active trees can have ancestors in common without fear of
* thread corruption problems.
*
* Internally, any single tree generation is simply stored as the
* set of changes between itself and its most recent ancestor (its parent).
* This compact delta representation allows chains of element trees to
* be created at relatively low cost. Clients of the ElementTree can
* instantaneously "undo" sets of changes by navigating up to the parent
* tree using the <code>getParent()</code> method.
*
* Although the delta representation is compact, extremely long delta
* chains make for a large structure that is potentially slow to query.
* For this reason, the client is encouraged to minimize delta chain
* lengths using the <code>collapsing(int)</code> and <code>makeComplete()</code>
* methods. The <code>getDeltaDepth()</code> method can be used to
* discover the length of the delta chain. The entire delta chain can
* also be re-oriented in terms of the current element tree using the
* <code>reroot()</code> operation.
*
* Classes are also available for tree serialization and navigation.
* @see ElementTreeReader
* @see ElementTreeWriter
* @see ElementTreeIterator
*
* Finally, why are ElementTrees in a package called "watson"?
* - "It's ElementTree my dear Watson, ElementTree."
*/
public class ElementTree {
protected DeltaDataTree tree;
protected IElementTreeData userData;
private class ChildIDsCache {
ChildIDsCache(IPath path, IPath[] childPaths) {
this.path = path;
this.childPaths = childPaths;
}
IPath path;
IPath[] childPaths;
}
private volatile ChildIDsCache childIDsCache = null;
private volatile DataTreeLookup lookupCache = null;
private volatile DataTreeLookup lookupCacheIgnoreCase = null;
private static int treeCounter = 0;
private int treeStamp;
/**
* Creates a new empty element tree.
*/
public ElementTree() {
initialize(new DeltaDataTree());
}
/**
* Creates an element tree given its internal node representation.
*/
protected ElementTree(DataTreeNode rootNode) {
initialize(rootNode);
}
/**
* Creates a new element tree with the given data tree as its representation.
*/
protected ElementTree(DeltaDataTree tree) {
initialize(tree);
}
/**
* Creates a new empty delta element tree having the
* given tree as its parent.
*/
protected ElementTree(ElementTree parent) {
if (!parent.isImmutable()) {
parent.immutable();
}
/* copy the user data forward */
IElementTreeData data = parent.getTreeData();
if (data != null) {
userData = (IElementTreeData) data.clone();
}
initialize(parent.tree.newEmptyDeltaTree());
}
/**
* Collapses this tree so that the given ancestor becomes its
* immediate parent. Afterwards, this tree will still have exactly the
* same contents, but its internal structure will be compressed.
*
* <p> This operation should be used to collapse chains of
* element trees created by newEmptyDelta()/immutable().
*
* <p>This element tree must be immutable at the start of this operation,
* and will be immutable afterwards.
* @return this tree.
*/
public synchronized ElementTree collapseTo(ElementTree parent) {
Assert.isTrue(tree.isImmutable());
if (this == parent) {
//already collapsed
return this;
}
//collapse my tree to be a forward delta of the parent's tree.
tree.collapseTo(parent.tree, DefaultElementComparator.getComparator());
return this;
}
/**
* Creates the indicated element and sets its element info.
* The parent element must be present, otherwise an IllegalArgumentException
* is thrown. If the indicated element is already present in the tree,
* its element info is replaced and any existing children are
* deleted.
*
* @param key element key
* @param data element data, or <code>null</code>
*/
public synchronized void createElement(IPath key, Object data) {
/* don't allow modification of the implicit root */
if (key.isRoot())
return;
// Clear the child IDs cache in case it's referring to this parent. This is conservative.
childIDsCache = null;
IPath parent = key.removeLastSegments(1);
try {
tree.createChild(parent, key.lastSegment(), data);
} catch (ObjectNotFoundException e) {
elementNotFound(parent);
}
// Set the lookup to be this newly created object.
lookupCache = DataTreeLookup.newLookup(key, true, data, true);
lookupCacheIgnoreCase = null;
}
/**
* Creates or replaces the subtree below the given path with
* the given tree. The subtree can only have one child below
* the root, which will become the node specified by the given
* key in this tree.
*
* @param key The path of the new subtree in this tree.
* @see #getSubtree(IPath)
*/
public synchronized void createSubtree(IPath key, ElementTree subtree) {
/* don't allow creating subtrees at the root */
if (key.isRoot()) {
throw new IllegalArgumentException(Messages.watson_noModify);
}
// Clear the child IDs cache in case it's referring to this parent.
// This is conservative.
childIDsCache = null;
// Clear the lookup cache, in case the element being created is the same
// as for the last lookup.
lookupCache = lookupCacheIgnoreCase = null;
try {
/* don't copy the implicit root node of the subtree */
IPath[] children = subtree.getChildren(subtree.getRoot());
if (children.length != 1) {
throw new IllegalArgumentException(Messages.watson_illegalSubtree);
}
/* get the subtree for the specified key */
DataTreeNode node = (DataTreeNode) subtree.tree.copyCompleteSubtree(children[0]);
/* insert the subtree in this tree */
tree.createSubtree(key, node);
} catch (ObjectNotFoundException e) {
elementNotFound(key);
}
}
/**
* Deletes the indicated element and its descendents.
* The element must be present.
*/
public synchronized void deleteElement(IPath key) {
/* don't allow modification of the implicit root */
if (key.isRoot())
return;
// Clear the child IDs cache in case it's referring to this parent.
// This is conservative.
childIDsCache = null;
// Clear the lookup cache, in case the element being deleted is the same
// as for the last lookup.
lookupCache = lookupCacheIgnoreCase = null;
try {
tree.deleteChild(key.removeLastSegments(1), key.lastSegment());
} catch (ObjectNotFoundException e) {
elementNotFound(key);
}
}
/**
* Complains that an element was not found
*/
protected void elementNotFound(IPath key) {
throw new IllegalArgumentException(NLS.bind(Messages.watson_elementNotFound, key));
}
/**
* Given an array of element trees, returns the index of the
* oldest tree. The oldest tree is the tree such that no
* other tree in the array is a descendent of that tree.
* Note that this counter-intuitive concept of oldest is based on the
* ElementTree orientation such that the complete tree is always the
* newest tree.
*/
public static int findOldest(ElementTree[] trees) {
/* first put all the trees in a hashtable */
HashMap<ElementTree, ElementTree> candidates = new HashMap<>((int) (trees.length * 1.5 + 1));
for (int i = 0; i < trees.length; i++) {
candidates.put(trees[i], trees[i]);
}
/* keep removing parents until only one tree remains */
ElementTree oldestSoFar = null;
while (candidates.size() > 0) {
/* get a new candidate */
ElementTree current = candidates.values().iterator().next();
/* remove this candidate from the table */
candidates.remove(current);
/* remove all of this element's parents from the list of candidates*/
ElementTree parent = current.getParent();
/* walk up chain until we hit the root or a tree we have already tested */
while (parent != null && parent != oldestSoFar) {
candidates.remove(parent);
parent = parent.getParent();
}
/* the current candidate is the oldest tree seen so far */
oldestSoFar = current;
/* if the table is now empty, we have a winner */
}
Assert.isNotNull(oldestSoFar);
/* return the appropriate index */
for (int i = 0; i < trees.length; i++) {
if (trees[i] == oldestSoFar) {
return i;
}
}
Assert.isTrue(false, "Should not get here"); //$NON-NLS-1$
return -1;
}
/**
* Returns the number of children of the element
* specified by the given path.
* The given element must be present in this tree.
*/
public synchronized int getChildCount(IPath key) {
Assert.isNotNull(key);
return getChildIDs(key).length;
}
/**
* Returns the IDs of the children of the specified element.
* If the specified element is null, returns the root element path.
*/
protected IPath[] getChildIDs(IPath key) {
ChildIDsCache cache = childIDsCache; // Grab it in case it's replaced concurrently.
if (cache != null && cache.path == key) {
return cache.childPaths;
}
try {
if (key == null)
return new IPath[] {tree.rootKey()};
IPath[] children = tree.getChildren(key);
childIDsCache = new ChildIDsCache(key, children); // Cache the result
return children;
} catch (ObjectNotFoundException e) {
elementNotFound(key);
return null; // can't get here
}
}
/**
* Returns the paths of the children of the element
* specified by the given path.
* The given element must be present in this tree.
*/
public synchronized IPath[] getChildren(IPath key) {
Assert.isNotNull(key);
return getChildIDs(key);
}
/**
* Returns the internal data tree.
*/
public DeltaDataTree getDataTree() {
return tree;
}
/**
* Returns the element data for the given element identifier.
* The given element must be present in this tree.
*/
public synchronized Object getElementData(IPath key) {
/* don't allow modification of the implicit root */
if (key.isRoot())
return null;
DataTreeLookup lookup = lookupCache; // Grab it in case it's replaced concurrently.
if (lookup == null || lookup.key != key)
lookupCache = lookup = tree.lookup(key);
if (lookup.isPresent)
return lookup.data;
elementNotFound(key);
return null; // can't get here
}
/**
* Returns the element data for the given element identifier.
* The given element must be present in this tree.
*/
public synchronized Object getElementDataIgnoreCase(IPath key) {
/* don't allow modification of the implicit root */
if (key.isRoot())
return null;
DataTreeLookup lookup = lookupCacheIgnoreCase; // Grab it in case it's replaced concurrently.
if (lookup == null || lookup.key != key)
lookupCacheIgnoreCase = lookup = tree.lookupIgnoreCase(key);
if (lookup.isPresent)
return lookup.data;
elementNotFound(key);
return null; // can't get here
}
/**
* Returns the names of the children of the specified element.
* The specified element must exist in the tree.
* If the specified element is null, returns the root element path.
*/
public synchronized String[] getNamesOfChildren(IPath key) {
try {
if (key == null)
return new String[] {""}; //$NON-NLS-1$
return tree.getNamesOfChildren(key);
} catch (ObjectNotFoundException e) {
elementNotFound(key);
return null; // can't get here
}
}
/**
* Returns the parent tree, or <code>null</code> if there is no parent.
*/
public ElementTree getParent() {
DeltaDataTree parentTree = tree.getParent();
if (parentTree == null) {
return null;
}
// The parent ElementTree is stored as the node data of the parent DeltaDataTree,
// to simplify canonicalization in the presence of rerooting.
return (ElementTree) parentTree.getData(tree.rootKey());
}
/**
* Returns the root node of this tree.
*/
public IPath getRoot() {
return getChildIDs(null)[0];
}
/**
* Returns the subtree rooted at the given key. In the resulting tree,
* the implicit root node (designated by Path.ROOT), has a single child,
* which is the node specified by the given key in this tree.
*
* The subtree must be present in this tree.
*
* @see #createSubtree(IPath, ElementTree)
*/
public ElementTree getSubtree(IPath key) {
/* the subtree of the root of this tree is just this tree */
if (key.isRoot()) {
return this;
}
try {
DataTreeNode elementNode = (DataTreeNode) tree.copyCompleteSubtree(key);
return new ElementTree(elementNode);
} catch (ObjectNotFoundException e) {
elementNotFound(key);
return null;
}
}
/**
* Returns the user data associated with this tree.
*/
public IElementTreeData getTreeData() {
return userData;
}
/**
* Returns true if there have been changes in the tree between the two
* given layers. The two must be related and new must be newer than old.
* That is, new must be an ancestor of old.
*/
public static boolean hasChanges(ElementTree newLayer, ElementTree oldLayer, IElementComparator comparator, boolean inclusive) {
// if any of the layers are null, assume that things have changed
if (newLayer == null || oldLayer == null)
return true;
if (newLayer == oldLayer)
return false;
//if the tree data has changed, then the tree has changed
if (comparator.compare(newLayer.getTreeData(), oldLayer.getTreeData()) != IElementComparator.K_NO_CHANGE)
return true;
// The tree structure has the top layer(s) (i.e., tree) parentage pointing down to a complete
// layer whose parent is null. The bottom layers (i.e., operationTree) point up to the
// common complete layer whose parent is null. The complete layer moves up as
// changes happen. To see if any changes have happened, we should consider only
// layers whose parent is not null. That is, skip the complete layer as it will clearly not be
// empty.
// look down from the current layer (always inclusive) if the top layer is mutable
ElementTree stopLayer = null;
if (newLayer.isImmutable())
// if the newLayer is immutable, the tree structure all points up so ensure that
// when searching up, we stop at newLayer (inclusive)
stopLayer = newLayer.getParent();
else {
ElementTree layer = newLayer;
while (layer != null && layer.getParent() != null) {
if (!layer.getDataTree().isEmptyDelta())
return true;
layer = layer.getParent();
}
}
// look up from the layer at which we started to null or newLayer's parent (variably inclusive)
// depending on whether newLayer is mutable.
ElementTree layer = inclusive ? oldLayer : oldLayer.getParent();
while (layer != null && layer.getParent() != stopLayer) {
if (!layer.getDataTree().isEmptyDelta())
return true;
layer = layer.getParent();
}
// didn't find anything that changed
return false;
}
/**
* Makes this tree immutable (read-only); ignored if it is already
* immutable.
*/
public synchronized void immutable() {
if (!tree.isImmutable()) {
tree.immutable();
/* need to clear the lookup cache since it reports whether results were found
in the topmost delta, and the order of deltas is changing */
lookupCache = lookupCacheIgnoreCase = null;
/* reroot the delta chain at this tree */
tree.reroot();
}
}
/**
* Returns true if this element tree includes an element with the given
* key, false otherwise.
*/
public synchronized boolean includes(IPath key) {
DataTreeLookup lookup = lookupCache; // Grab it in case it's replaced concurrently.
if (lookup == null || lookup.key != key) {
lookupCache = lookup = tree.lookup(key);
}
return lookup.isPresent;
}
/**
* Returns true if this element tree includes an element with the given
* key, ignoring the case of the key, and false otherwise.
*/
public boolean includesIgnoreCase(IPath key) {
DataTreeLookup lookup = lookupCacheIgnoreCase; // Grab it in case it's replaced concurrently.
if (lookup == null || lookup.key != key) {
lookupCacheIgnoreCase = lookup = tree.lookupIgnoreCase(key);
}
return lookup.isPresent;
}
protected void initialize(DataTreeNode rootNode) {
/* create the implicit root node */
initialize(new DeltaDataTree(new DataTreeNode(null, null, new AbstractDataTreeNode[] {rootNode})));
}
protected void initialize(DeltaDataTree newTree) {
// Keep this element tree as the data of the root node.
// Useful for canonical results for ElementTree.getParent().
// see getParent().
treeStamp = treeCounter++;
newTree.setData(newTree.rootKey(), this);
this.tree = newTree;
}
/**
* Returns whether this tree is immutable.
*/
public boolean isImmutable() {
return tree.isImmutable();
}
/**
* Merges a chain of deltas for a certain subtree to this tree.
* If this tree has any data in the specified subtree, it will
* be overwritten. The receiver tree must be open, and it will
* be made immutable during the merge operation. The trees in the
* provided array will be replaced by new trees that have been
* merged into the receiver's delta chain.
*
* @param path The path of the subtree chain to merge
* @param trees The chain of trees to merge. The trees can be
* in any order, but they must all form a simple ancestral chain.
* @return A new open tree with the delta chain merged in.
*/
public ElementTree mergeDeltaChain(IPath path, ElementTree[] trees) {
if (path == null || trees == null) {
throw new IllegalArgumentException(NLS.bind(Messages.watson_nullArg, "ElementTree.mergeDeltaChain")); //$NON-NLS-1$
}
/* The tree has to be open */
if (isImmutable()) {
throw new IllegalArgumentException(Messages.watson_immutable);
}
ElementTree current = this;
if (trees.length > 0) {
/* find the oldest tree to be merged */
ElementTree toMerge = trees[findOldest(trees)];
/* merge the trees from oldest to newest */
while (toMerge != null) {
if (path.isRoot()) {
//copy all the children
IPath[] children = toMerge.getChildren(Path.ROOT);
for (int i = 0; i < children.length; i++) {
current.createSubtree(children[i], toMerge.getSubtree(children[i]));
}
} else {
//just copy the specified node
current.createSubtree(path, toMerge.getSubtree(path));
}
current.immutable();
/* replace the tree in the array */
/* we have to go through all trees because there may be duplicates */
for (int i = 0; i < trees.length; i++) {
if (trees[i] == toMerge) {
trees[i] = current;
}
}
current = current.newEmptyDelta();
toMerge = toMerge.getParent();
}
}
return current;
}
/**
* Creates a new element tree which is represented as a delta on this one.
* Initially they have the same content. Subsequent changes to the new
* tree will not affect this one.
*/
public synchronized ElementTree newEmptyDelta() {
// Don't want old trees hanging onto cached infos.
lookupCache = lookupCacheIgnoreCase = null;
return new ElementTree(this);
}
/**
* Returns a mutable copy of the element data for the given path.
* This copy will be held onto in the most recent delta.
* ElementTree data MUST implement the IElementTreeData interface
* for this method to work. If the data does not define that interface
* this method will fail.
*/
public synchronized Object openElementData(IPath key) {
Assert.isTrue(!isImmutable());
/* don't allow modification of the implicit root */
if (key.isRoot())
return null;
DataTreeLookup lookup = lookupCache; // Grab it in case it's replaced concurrently.
if (lookup == null || lookup.key != key) {
lookupCache = lookup = tree.lookup(key);
}
if (lookup.isPresent) {
if (lookup.foundInFirstDelta)
return lookup.data;
/**
* The node has no data in the most recent delta.
* Pull it up to the present delta by setting its data with a clone.
*/
IElementTreeData oldData = (IElementTreeData) lookup.data;
if (oldData != null) {
try {
Object newData = oldData.clone();
tree.setData(key, newData);
lookupCache = lookupCacheIgnoreCase = null;
return newData;
} catch (ObjectNotFoundException e) {
elementNotFound(key);
}
}
} else {
elementNotFound(key);
}
return null;
}
/**
* Sets the element for the given element identifier.
* The given element must be present in this tree.
* @param key element identifier
* @param data element info, or <code>null</code>
*/
public synchronized void setElementData(IPath key, Object data) {
/* don't allow modification of the implicit root */
if (key.isRoot())
return;
Assert.isNotNull(key);
// Clear the lookup cache, in case the element being modified is the same
// as for the last lookup.
lookupCache = lookupCacheIgnoreCase = null;
try {
tree.setData(key, data);
} catch (ObjectNotFoundException e) {
elementNotFound(key);
}
}
/**
* Sets the user data associated with this tree.
*/
public void setTreeData(IElementTreeData data) {
userData = data;
}
/* (non-Javadoc)
* Method declared on IStringPoolParticipant
*/
public void shareStrings(StringPool set) {
tree.storeStrings(set);
}
/**
* Returns a string representation of this element tree's
* structure suitable for debug purposes.
*/
public String toDebugString() {
final StringBuilder buffer = new StringBuilder("\n"); //$NON-NLS-1$
IElementContentVisitor visitor = new IElementContentVisitor() {
@Override
public boolean visitElement(ElementTree aTree, IPathRequestor elementID, Object elementContents) {
buffer.append(elementID.requestPath() + " " + elementContents + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
return true;
}
};
new ElementTreeIterator(this, Path.ROOT).iterate(visitor);
return buffer.toString();
}
@Override
public String toString() {
return "ElementTree(" + treeStamp + ")"; //$NON-NLS-1$ //$NON-NLS-2$
}
}