![JAR search and dependency download from the Maven repository](/logo.png)
ca.odell.glazedlists.TreeList Maven / Gradle / Ivy
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.impl.GlazedListsImpl;
import ca.odell.glazedlists.impl.adt.CircularArrayList;
import ca.odell.glazedlists.impl.adt.KeyedCollection;
import ca.odell.glazedlists.impl.adt.barcode2.Element;
import ca.odell.glazedlists.impl.adt.barcode2.FourColorTree;
import ca.odell.glazedlists.impl.adt.barcode2.ListToByteCoder;
import java.util.*;
/**
* A hierarchial EventList that infers its structure from a flat list.
*
* Warning: This class is
* thread ready but not thread safe. See {@link EventList} for an example
* of thread safe code.
*
*
Developer Preview this class is still under development
* and subject to API changes.
*
* @author Jesse Wilson
*/
public final class TreeList extends TransformedList,E> {
/** An {@link ExpansionModel} with a simple policy: all nodes start expanded. */
public static final ExpansionModel NODES_START_EXPANDED = new DefaultExpansionModel(true);
/** An {@link ExpansionModel} with a simple policy: all nodes start collapsed. */
public static final ExpansionModel NODES_START_COLLAPSED = new DefaultExpansionModel(false);
/**
* @return an {@link ExpansionModel} with a simple policy: all nodes start expanded.
*/
@SuppressWarnings("unchecked")
public static final ExpansionModel nodesStartExpanded() {
return NODES_START_EXPANDED;
}
/**
* @return an {@link ExpansionModel} with a simple policy: all nodes start collapsed.
*/
@SuppressWarnings("unchecked")
public static final ExpansionModel nodesStartCollapsed() {
return NODES_START_COLLAPSED;
}
/** marker values for comparing elements */
private static final Element MINIMUM_ELEMENT = new FakeElement();
private static final Element MAXIMUM_ELEMENT = new FakeElement();
private static final FunctionList.Function NO_OP_FUNCTION = new NoOpFunction();
/** determines the layout of new nodes as they are created */
private ExpansionModel expansionModel;
/** node colors define where it is in the source and where it is here */
private static final ListToByteCoder BYTE_CODER = new ListToByteCoder(Arrays.asList("R", "V", "r", "v"));
private static final byte VISIBLE_REAL = BYTE_CODER.colorToByte("R");
private static final byte VISIBLE_VIRTUAL = BYTE_CODER.colorToByte("V");
private static final byte HIDDEN_REAL = BYTE_CODER.colorToByte("r");
private static final byte HIDDEN_VIRTUAL = BYTE_CODER.colorToByte("v");
/** node classes let us search through nodes more efficiently */
private static final byte ALL_NODES = BYTE_CODER.colorsToByte(Arrays.asList("R", "V", "r", "v"));
private static final byte VISIBLE_NODES = BYTE_CODER.colorsToByte(Arrays.asList("R", "V"));
private static final byte HIDDEN_NODES = BYTE_CODER.colorsToByte(Arrays.asList("r", "v"));
private static final byte REAL_NODES = BYTE_CODER.colorsToByte(Arrays.asList("R", "r"));
/** compare nodes by value */
private final NodeComparator nodeComparator;
/** an {@link EventList} of uncollapsed {@link Node}s with the structure of the tree */
private EventList> nodesList;
/** a readonly {@link List} of all {@link Node}s regardless of their collapsed state */
private List> allNodesList;
/** initialization data is only necessary to later dispose the TreeList */
private InitializationData initializationData;
/**
* All the tree data is stored in this tree.
*
* Children of collapsed nodes are {@link #VISIBLE_NODES}, everything
* else is {@link #HIDDEN_NODES}.
*/
private FourColorTree> data = new FourColorTree>(BYTE_CODER);
/**
* The format is used to obtain path information from list elements.
*/
private Format format;
/**
* Create a new TreeList that adds hierarchy to the specified source list.
* This constructor does not sort the elements.
*/
public TreeList(EventList source, Format format, ExpansionModel expansionModel) {
this(new InitializationData(source, format, expansionModel));
}
/** master Constructor */
private TreeList(InitializationData initializationData) {
super(initializationData.getSource());
this.format = initializationData.format;
this.nodeComparator = initializationData.nodeComparator;
this.expansionModel = initializationData.expansionModel;
this.initializationData = initializationData;
// insert the new elements like they were adds
NodeAttacher nodeAttacher = new NodeAttacher(false);
for(int i = 0; i < super.source.size(); i++) {
Node node = super.source.get(i);
node.expanded = expansionModel.isExpanded(node.getElement(), node.path);
addNode(node, HIDDEN_REAL, i);
nodeAttacher.nodesToAttach.queueNewNodeForInserting(node);
}
// attach siblings and parent nodes
nodeAttacher.attachAll();
assert(isValid());
source.addListEventListener(this);
}
/** @deprecated use the constructor that takes an {@link ExpansionModel} */
@Deprecated
public TreeList(EventList source, Format format) {
this(new InitializationData(source, format, TreeList.nodesStartExpanded()));
}
/**
* Helper class for managing various amounts of transitional
* objects required by the class constructor.
*/
private static class InitializationData {
private final Format format;
private final ExpansionModel expansionModel;
private final NodeComparator nodeComparator;
private final FunctionList> sourceNodes;
private final SortedList> sortedList;
public InitializationData(EventList sourceElements, Format format, ExpansionModel expansionModel) {
this.format = format;
this.expansionModel = expansionModel;
this.sourceNodes = new FunctionList>(sourceElements, new ElementToTreeNodeFunction(format, expansionModel), NO_OP_FUNCTION);
this.nodeComparator = comparatorToNodeComparator(format);
this.sortedList = new SortedList>(sourceNodes, nodeComparator);
}
/**
* @return the EventList to be wrapped directly by the {@link TreeList}. This varies
* depending on whether this tree is sorted or unsorted.
*/
public EventList> getSource() {
if(sortedList != null) return sortedList;
return sourceNodes;
}
public void dispose() {
if(sortedList != null) {
sortedList.dispose();
}
sourceNodes.dispose();
}
}
/**
* @return an {@link EventList} of {@link Node}s which can be used
* to access this tree more structurally.
*/
public EventList> getNodesList() {
if(nodesList == null) {
nodesList = new NodesList();
}
return nodesList;
}
/**
* @return a readonly {@link List} of all {@link Node}s in
* the tree regardless of their collapsed state
*/
List> getAllNodesList() {
if(allNodesList == null) {
allNodesList = new AllNodesList();
}
return allNodesList;
}
/**
* @return the number of ancestors of the node at the specified index.
* Root nodes have depth 0, other nodes depth is one
* greater than the depth of their parent node.
*/
public int depth(int visibleIndex) {
return getTreeNode(visibleIndex).path.size() - 1;
}
/**
* Get the size of the subtree at the specified index, counting nodes
* of the specified types only.
*
* @param index either a visible or invisible index of the node in the tree
* whose subtree size is to be measured
* @param indexIsVisibleIndex true
if index is a visible index,
* false if it's an overall index
* @param includeCollapsedNodes true
if the result should include
* nodes not visible in the fully-expanded tree, false
if the
* result should be restricted to only those nodes that are visible.
*/
private int subtreeSize(int index, boolean indexIsVisibleIndex, boolean includeCollapsedNodes) {
byte coloursIn = indexIsVisibleIndex ? VISIBLE_NODES : ALL_NODES;
byte coloursOut = includeCollapsedNodes ? ALL_NODES : VISIBLE_NODES;
// the node whose subtree is being measured
Node node = data.get(index, coloursIn).get();
// get the index in terms of collapsed nodes
int indexOut;
if(coloursIn == coloursOut) {
indexOut = index;
} else {
// we can't get the collapsed subtree size of a collapsed node
assert((node.element.getColor() & coloursOut) != 0);
indexOut = data.convertIndexColor(index, coloursIn, coloursOut);
}
// find the next node that's not a child to find the delta
Node nextNodeNotInSubtree = nextNodeThatsNotAChildOfByStructure(node);
// if we don't have a sibling after us, we've hit the end of the tree
if(nextNodeNotInSubtree == null) {
return data.size(coloursOut) - indexOut;
}
return data.indexOfNode(nextNodeNotInSubtree.element, coloursOut) - indexOut;
}
/**
* The number of nodes including the node itself in its subtree.
*/
public int subtreeSize(int visibleIndex, boolean includeCollapsed) {
return subtreeSize(visibleIndex, true, includeCollapsed);
}
/**
* Find the first node after the specified node that's not its child. This
* is necessary to calculate the size of the node's subtree.
*/
private Node nextNodeThatsNotAChildOfByStructure(Node node) {
for(; node != null; node = node.parent) {
Node followerNode = node.siblingAfter;
if(followerNode != null) {
return followerNode;
}
}
return null;
}
/** {@inheritDoc} */
@Override
public int size() {
return data.size(VISIBLE_NODES);
}
/** {@inheritDoc} */
@Override
protected boolean isWritable() {
return true;
}
/** {@inheritDoc} */
@Override
protected int getSourceIndex(int mutationIndex) {
return data.convertIndexColor(mutationIndex, VISIBLE_NODES, REAL_NODES);
}
/** {@inheritDoc} */
@Override
public E get(int visibleIndex) {
return getTreeNode(visibleIndex).getElement();
}
/** {@inheritDoc} */
@Override
public E remove(int visibleIndex) {
final E result = get(visibleIndex);
super.remove(visibleIndex);
return result;
}
/**
* @return the tree node at the specified index
*/
public Node getTreeNode(int visibleIndex) {
return data.get(visibleIndex, VISIBLE_NODES).get();
}
/**
* @return true
if the node at the specified index has
* children, regardless of whether such children are visible.
*/
public boolean hasChildren(int visibleIndex) {
boolean hasChildren = subtreeSize(visibleIndex, true) > 1;
boolean isLeaf = getTreeNode(visibleIndex).isLeaf();
if(isLeaf == hasChildren) {
subtreeSize(visibleIndex, true, true);
}
return hasChildren;
}
/**
* Change how the structure of the tree is derived.
*
* @param treeFormat
*/
public void setTreeFormat(Format treeFormat) {
// sourceNodes.setFunction(new ElementToTreeNodeFunction(treeFormat));
}
/**
* Get a {@link List} containing all {@link Node}s with no parents in the
* tree.
*/
public List> getRoots() {
// todo: make this fast
List> result = new ArrayList>();
for(int i = 0; i < size(); i++) {
Node possibleRoot = getTreeNode(i);
if(possibleRoot.pathLength() == 1) {
result.add(possibleRoot);
}
}
return result;
}
/**
* Whether the value at the specified index can have child nodes or not.
*
* @see Format#allowsChildren
* @return true if child nodes can be added to the
* specified node.
*/
public boolean getAllowsChildren(int visibleIndex) {
return format.allowsChildren(get(visibleIndex));
}
/**
* @return true
if the children of the node at the specified
* index are visible. Nodes with no children may be expanded or not,
* this is used to determine whether to show children should any be
* added.
*/
public boolean isExpanded(int visibleIndex) {
return data.get(visibleIndex, VISIBLE_NODES).get().expanded;
}
/**
* Control whether the child elements of the specified node are visible.
*
* @param expanded true
to expand the node, false
* to collapse it.
*/
public void setExpanded(int visibleIndex, boolean expanded) {
Node toExpand = data.get(visibleIndex, VISIBLE_NODES).get();
expansionModel.setExpanded(toExpand.getElement(), toExpand.path, expanded);
setExpanded(toExpand, expanded);
assert(isValid());
}
/**
* Internally control the expanded/collapsed state of a node without
* reporting the change to the external {@llink ExpansionModel}. This
* is useful when expand/collapsed state changes as nodes are split and
* merged due to tree structure changes.
*/
private void setExpanded(Node toExpand, boolean expanded) {
// if we're already in the desired state, give up!
if(toExpand.expanded == expanded) return;
// toggle the active node.
toExpand.expanded = expanded;
// whether the entire subtree, including the specified node, is visible
boolean subtreeIsVisible = (toExpand.element.getColor() & VISIBLE_NODES) != 0;
// only fire events and change deeper elements if the subtree is showing
if(subtreeIsVisible) {
updates.beginEvent();
// This node's visibility does not change, only that of its children
if(toExpand.isVisible()) {
int visibleIndex = data.indexOfNode(toExpand.element, VISIBLE_NODES);
updates.addUpdate(visibleIndex);
}
Node toExpandNextSibling = nextNodeThatsNotAChildOfByStructure(toExpand);
// walk through the subtree, looking for all the descendents we need
// to change. As we encounter them, change them and fire events
for(Node descendent = toExpand.next(); descendent != null && descendent != toExpandNextSibling; descendent = descendent.next()) {
// figure out if this node should be visible by walking up the ancestors
// to the node being expanded, searching for a parent that's not
// expanded
boolean shouldBeVisible = expanded;
for(Node ancestor = descendent.parent; shouldBeVisible && ancestor != toExpand; ancestor = ancestor.parent) {
if(!ancestor.expanded) {
shouldBeVisible = false;
}
}
if(shouldBeVisible == descendent.isVisible()) continue;
// show a non-visible node
if(shouldBeVisible) {
setVisible(descendent, true);
int insertIndex = data.indexOfNode(descendent.element, VISIBLE_NODES);
updates.elementInserted(insertIndex, descendent.getElement());
// hide a visible node
} else {
int deleteIndex = data.indexOfNode(descendent.element, VISIBLE_NODES);
updates.elementDeleted(deleteIndex, descendent.getElement());
setVisible(descendent, false);
}
}
updates.commitEvent();
}
}
/**
* A convenience method to expand the row if it is currently collapsed or
* vice versa.
*/
public void toggleExpanded(int visibleIndex) {
setExpanded(visibleIndex, !isExpanded(visibleIndex));
}
/**
* Set the visibility of the specified node without firing any events.
*/
private void setVisible(Node node, boolean visible) {
byte newColor;
if(visible) {
newColor = node.virtual ? VISIBLE_VIRTUAL : VISIBLE_REAL;
} else {
newColor = node.virtual ? HIDDEN_VIRTUAL : HIDDEN_REAL;
}
data.setColor(node.element, newColor);
}
/**
* Set the virtualness of the specified node without firing events.
*/
private void setVirtual(Node node, boolean virtual) {
byte newColor;
if(virtual) {
newColor = node.isVisible() ? VISIBLE_VIRTUAL : HIDDEN_VIRTUAL;
} else {
newColor = node.isVisible() ? VISIBLE_REAL : HIDDEN_REAL;
}
data.setColor(node.element, newColor);
}
/** {@inheritDoc} */
@Override
public void listChanged(ListEvent> listChanges) {
updates.beginEvent(true);
// first pass: apply changes to the trees structure, marking all new
// nodes as hidden. In the next pass we'll figure out parents, siblings
// and fire events for nodes that shouldn't be hidden
List> nodesToVerify = new ArrayList>();
NodeAttacher nodeAttacher = new NodeAttacher(true);
FinderInserter finderInserter = new FinderInserter();
while(listChanges.next()) {
int sourceIndex = listChanges.getIndex();
int type = listChanges.getType();
if(type == ListEvent.INSERT) {
Node inserted = finderInserter.findOrInsertNode(sourceIndex);
nodeAttacher.nodesToAttach.queueNewNodeForInserting(inserted);
} else if(type == ListEvent.UPDATE) {
replaceAndDetachNode( sourceIndex, nodesToVerify );
Node updated = finderInserter.findOrInsertNode(sourceIndex);
nodeAttacher.nodesToAttach.queueNewNodeForInserting(updated);
} else if(type == ListEvent.DELETE) {
deleteAndDetachNode(sourceIndex, nodesToVerify);
}
}
// second pass: walk through all the changed nodes and attach parents
// and siblings, plus fire events for all the inserted or updated nodes.
nodeAttacher.attachAll();
// blow away obsolete virtual leaf nodes now that we can know for sure
// if they're obsolete this doesn't depend on the siblings & parents to be attached
deleteObsoleteVirtualLeaves(nodesToVerify);
// blow away obsolete parent nodes now that we can know for sure if they're obsolete
deleteObsoleteVirtualParents(nodesToVerify);
assert(isValid());
updates.commitEvent();
}
/**
* A transient helper object to attach nodes to sibling and parent nodes.
*
* Because this class is stateful, it shouldn't be referenced across changes
* as that risks a memory leak.
*/
private class NodeAttacher {
private final boolean fireEvents;
/** the queue of nodes needing parents and siblings attached */
private final NodesToAttach nodesToAttach = new NodesToAttach();
/** the node having its parents and siblings attached */
private Node current;
/** the active node before current which we're hoping is current's parent */
private Node predecessor;
/** the active node before current which we're hoping is current's sibling */
private Node predecessorAtOurHeight;
/** the path from the changed node to the root, used to fire events in the second phase */
private List> pathToRoot = new ArrayList>();
/** the index of the changed node, which is where parent nodes shall be inserted */
private int index;
/** provide expand/collapsed state for nodes that are inserted or split */
private ExpansionModel expansionModel;
/** expand newly-inserted parent nodes when we discover a visible child */
private List> nodesToExpand = new ArrayList>();
public NodeAttacher(boolean fireEvents) {
this.fireEvents = fireEvents;
}
public void attachAll() {
// attach nodes
while(!nodesToAttach.isEmpty()) {
Node changed = nodesToAttach.removeFirst();
boolean newlyInserted = nodesToAttach.getNewlyInsertedAndReset(changed);
attach(changed, newlyInserted);
}
// fix up the expanded states
for(Iterator> i = nodesToExpand.iterator(); i.hasNext(); ) {
setExpanded(i.next(), true);
}
nodesToExpand.clear();
}
/**
* Walk up the tree, fixing parents and siblings of the specified changed
* node. Performing this fix may require:
* attaching parents
* attaching siblings
* firing an 'insert' event for such parents and siblings
*
* @param changed the node to attach parents and siblings to
* @param newlyInserted true
if this node is a brand-new
* node in the tree, or false
if we're reattaching an
* existing node. This has significant consequences for the logic that
* manages expanded state and repairing siblings.
* @return true
if changes were made to the tree
*/
private void attach(Node changed, boolean newlyInserted) {
current = changed;
// prepare the expand/collapsed state of created nodes
if(newlyInserted) {
expansionModel = TreeList.this.expansionModel;
} else {
expansionModel = new CloneStateNewNodeStateModel(current);
}
// prepare state for attaching the node
index = data.indexOfNode(current.element, ALL_NODES);
predecessor = current.previous();
predecessorAtOurHeight = null;
// make sure the following node is repaired, as it might have become
// separated from its siblings or parent by the insertion of this node
if(newlyInserted) {
Node follower = current.next();
if(follower != null) {
nodesToAttach.queuePrefixForAttaching(follower);
}
}
attachParentsAndSiblings();
fixVisibilityAndFireEvents();
// cleanup
pathToRoot.clear();
}
private void attachParentsAndSiblings() {
boolean preexistingParentFound = false;
while(current != null) {
int currentPathLength = current.pathLength();
int predecessorPathLength = predecessor == null ? 0 : predecessor.pathLength();
// we've already found a connection, keep accumulating the path to root
if(preexistingParentFound) {
incrementCurrent();
// the predecessor is too short to be our parent, so create a new parent
// and hope that the predecessor is our grandparent
} else if(currentPathLength > predecessorPathLength + 1) {
createAndAttachParent();
// the predecessor is too tall to be our parent, maybe its parent is our parent?
} else if(predecessorPathLength >= currentPathLength) {
// make sure our predecessor's sibling has parents reattached if necessary
incrementPredecessor();
// sweet! the predecessor node is our parent!
} else if(isAncestorByValue(current, predecessor)) {
assert(currentPathLength == predecessorPathLength + 1);
attachParent(predecessor, predecessorAtOurHeight);
// we're mostly done, just fill in the path to root
preexistingParentFound = true;
// the predecessor node is not our parent, so create a new parent
// and hope we have common grandparents
} else {
assert(currentPathLength == predecessorPathLength + 1);
assert(predecessor != null);
createAndAttachParent();
// make sure our predecessor's sibling has parents reattached if necessary
incrementPredecessor();
}
}
}
private void incrementCurrent() {
pathToRoot.add(current);
current = current.parent;
}
private void incrementPredecessor() {
if(predecessor.siblingAfter != null && predecessor.siblingAfter != current) {
nodesToAttach.queueOutOfOrderNodeForAttaching(predecessor.siblingAfter);
predecessor.siblingAfter.siblingBefore = null;
predecessor.siblingAfter = null;
}
predecessorAtOurHeight = predecessor;
predecessor = predecessor.parent;
}
/**
* Attach the default parent for the specified node, using the node's
* ability to describe a prototype for its parent object.
*/
private void createAndAttachParent() {
Node parent = current.describeParent();
if(parent != null) {
parent.expanded = expansionModel.isExpanded(parent.getElement(), parent.path);
addNode(parent, HIDDEN_VIRTUAL, index);
}
attachParent(parent, null);
}
/**
* Attach the specified parent to the specified node.
*
* @param parent the parent node, may be null
* @param siblingBeforeNode the node immediately before the node of interest
* who is a child of the same parent. This will be linked in as the
* new node's sibling. If null
, no linking/unlinking will
* be performed.
*/
private void attachParent(Node parent, Node siblingBeforeNode) {
assert(current != null);
assert((current.pathLength() == 1 && parent == null) || (current.pathLength() == parent.pathLength() + 1));
// attach the siblings, the nearest child of our parent will become our sibling
// if it isn't already
if(siblingBeforeNode != null && siblingBeforeNode.siblingAfter != current) {
if(siblingBeforeNode.pathLength() != current.pathLength()) {
throw new IllegalStateException();
}
assert(siblingBeforeNode.parent == parent);
if(siblingBeforeNode.siblingAfter != null) {
assert(current.siblingAfter == null);
current.siblingAfter = siblingBeforeNode.siblingAfter;
siblingBeforeNode.siblingAfter.siblingBefore = current;
}
current.siblingBefore = siblingBeforeNode;
siblingBeforeNode.siblingAfter = current;
assert(current.siblingBefore != current);
assert(current.siblingAfter != current);
}
// attach the new parent to this and siblings after. This is necessary when
// siblings have been split from their previous parent
for(Node currentSibling = current; currentSibling != null; currentSibling = currentSibling.siblingAfter) {
currentSibling.parent = parent;
}
// force the parent node to be expanded if it's child is visible.
// this is necessary only when the parent is a new node and the
// expansionModel provided a collapsed state for a node with children
if(parent != null && !parent.expanded && current.isVisible()) {
nodesToExpand.add(parent);
}
// now the current node has shifted up to the parent node
incrementCurrent();
}
/**
* Fire events for the recently changed node, going from root down
*/
private void fixVisibilityAndFireEvents() {
boolean visible = true;
for(int i = pathToRoot.size() - 1; i >= 0; i--) {
current = pathToRoot.get(i);
// only fire events for visible nodes
if(visible) {
// an inserted node
if(!current.isVisible()) {
setVisible(current, true);
int visibleIndex = data.indexOfNode(current.element, VISIBLE_NODES);
if(fireEvents) {
updates.addInsert(visibleIndex);
}
// an updated node
} else {
int visibleIndex = data.indexOfNode(current.element, VISIBLE_NODES);
if(fireEvents) {
updates.addUpdate(visibleIndex);
}
}
}
// collapsed state restricts visibility on child elements
visible = visible && current.expanded;
}
}
}
/**
* A list of nodes to be attached to their parent nodes and siblings, provided
* in increasing order by index.
*
* This queue is necessary because our algorithm encounters nodes in random
* order but must process them in increasing order.
*/
private final class NodesToAttach {
private final List> nodes = new CircularArrayList>();
private final NodeIndexComparator nodeIndexComparator = new NodeIndexComparator();
/**
* Add this node to the queue in order.
* @param node
*/
private void queueOutOfOrderNodeForAttaching(Node node) {
int position = Collections.binarySearch(nodes, node, nodeIndexComparator);
if(position >= 0) return;
nodes.add(-position - 1, node);
assert(isValid());
}
/**
* Add this node to the beginning of the queue.
*/
private void queuePrefixForAttaching(Node node) {
if(!nodes.isEmpty()) {
if(nodes.get(0) == node) {
return;
}
assert(nodeIndexComparator.compare(nodes.get(0), node) >= 0);
}
nodes.add(0, node);
assert(isValid());
}
/**
* Add this node to the end of the queue.
*/
private void queueNewNodeForInserting(Node node) {
assert(nodes.isEmpty() || nodeIndexComparator.compare(nodes.get(nodes.size() - 1), node) < 0);
nodes.add(node);
node.isNewlyInserted = true;
assert(isValid());
}
private boolean isEmpty() {
return nodes.isEmpty();
}
private Node removeFirst() {
return nodes.remove(0);
}
private boolean getNewlyInsertedAndReset(Node node) {
boolean result = node.isNewlyInserted;
node.isNewlyInserted = false;
return result;
}
private boolean isValid() {
for(int i = 0; i < nodes.size() - 1; i++) {
Node a = nodes.get(i);
Node b = nodes.get(i + 1);
assert(nodeIndexComparator.compare(a, b) <= 0);
}
return true;
}
/**
* Compare two nodes by their position in the tree.
*/
private final class NodeIndexComparator implements Comparator> {
@Override
public int compare(Node a, Node b) {
if(a.element == null || b.element == null) {
throw new IllegalStateException();
}
return data.indexOfNode(a.element, ALL_NODES) - data.indexOfNode(b.element, ALL_NODES);
}
}
}
/**
* Short-lived helper class to finds and inserts nodes into the tree.
* Instances of this class should not be reused across ListEvents.
*/
private class FinderInserter {
final KeyedCollection>,List> indicesByValue;
public FinderInserter() {
this.indicesByValue = GlazedListsImpl.keyedCollection(
new ElementLocationComparator());
}
/**
* Handle a source insert at the specified index by adding the corresponding
* real node, or converting a virtual node to a real node. The real node
* is inserted, marked as real and hidden, and returned.
*
* @param sourceIndex the index of the element in the source list that has
* been inserted
* @return the new node, prior to any events fired
*/
private Node findOrInsertNode(int sourceIndex) {
Node inserted = source.get(sourceIndex);
inserted.resetDerivedState();
List insertedPath = inserted.path();
final int dataSize = data.size(ALL_NODES);
// bound the range of indices where this node can be inserted. This is
// all the virtual nodes between our predecessor and follower in the
// source list
int firstPossibleIndex = sourceIndex > 0
? data.convertIndexColor(sourceIndex - 1, REAL_NODES, ALL_NODES) + 1
: 0;
int lastPossibleIndex = sourceIndex < data.size(REAL_NODES)
? data.convertIndexColor(sourceIndex, REAL_NODES, ALL_NODES)
: data.size(ALL_NODES);
Element> firstPossibleElement = indexToElement(firstPossibleIndex, dataSize);
Element> lastPossibleElement = indexToElement(lastPossibleIndex, dataSize);
// make sure we have all the values available that we need
populateIndicesByValue(firstPossibleIndex, lastPossibleIndex);
// find a virtual node with the exact same value
Element> virtualSameElement = indicesByValue.find(firstPossibleElement, lastPossibleElement, insertedPath);
if(virtualSameElement != null) {
Node virtualSameNode = virtualSameElement.get();
if (!virtualSameNode.virtual) {
throw new IllegalStateException();
}
replaceNode(virtualSameNode, inserted, false);
return inserted;
}
// find a virtual node that's an ancestor by value
int insertIndex = firstPossibleIndex;
for (int i = insertedPath.size() - 1; i >= 0; i--) {
List pathOfLengthI = insertedPath.subList(0, i);
Element> bestAncestor = indicesByValue.find(firstPossibleElement, lastPossibleElement, pathOfLengthI);
if (bestAncestor != null) {
if (!bestAncestor.get().virtual) {
throw new IllegalStateException();
}
insertIndex = data.indexOfNode(bestAncestor, ALL_NODES) + 1;
break;
}
}
// insert the node as hidden by default - if we need to show this node,
// we'll change its state later and fire an 'insert' event then
addNode(inserted, HIDDEN_REAL, insertIndex);
return inserted;
}
/**
* Convert a possibly-virtual index (such as size, or 0 on an empty list) to
* the possibly-element that represents.
*/
private Element> indexToElement(int index, int dataSize) {
if (index >= 0 && index < dataSize) return data.get(index, ALL_NODES);
else if (index == 0) return MINIMUM_ELEMENT;
else if (index == dataSize) return MAXIMUM_ELEMENT;
else throw new IllegalArgumentException();
}
private void populateIndicesByValue(int firstNeeded, int lastNeeded) {
// fill in everything between firstNeeded and firstAvailable
Element> firstAvailableElement = indicesByValue.first();
int firstAvailable = firstAvailableElement != null
? data.indexOfNode(firstAvailableElement, ALL_NODES)
: lastNeeded;
populate(firstNeeded, firstAvailable);
// fill in everything between lastAvailable and lastNeeded
Element> lastAvailableElement = indicesByValue.last();
int lastAvailable = lastAvailableElement != null
? data.indexOfNode(lastAvailableElement, ALL_NODES)
: firstAvailable;
populate(lastAvailable + 1, lastNeeded);
}
/**
* Populate the indicesByValue collection with the available virtual nodes.
*/
private void populate(int start, int end) {
// we might be able to optimize this loop using element.next() ?
for(int i = start; i < end; i++) {
Element> element = data.get(i, ALL_NODES);
indicesByValue.insert(element, element.get().path());
}
}
private class ElementLocationComparator implements Comparator>> {
@Override
public int compare(Element> a, Element> b) {
if (a == b) {
return 0;
} else if (a == MINIMUM_ELEMENT || b == MAXIMUM_ELEMENT) {
return -1;
} else if (b == MINIMUM_ELEMENT || a == MAXIMUM_ELEMENT) {
return 1;
} else {
int aIndex = data.indexOfNode(a, ALL_NODES);
int bIndex = data.indexOfNode(b, ALL_NODES);
return aIndex - bIndex;
}
}
}
private class PathAsListComparator implements Comparator> {
@Override
public int compare(List a, List b) {
int aPathLength = a.size();
int bPathLength = b.size();
// compare by value first
for(int d = 0; d < aPathLength && d < bPathLength; d++) {
Comparator super E> comparator = format.getComparator(d);
if (comparator == null) return 0;
int result = comparator.compare(a.get(d), b.get(d));
if(result != 0) return result;
}
// and path length second
return aPathLength - bPathLength;
}
}
}
/**
* Replaces the node at the specified index, firing all the required
* notifications.
*/
private void replaceAndDetachNode(int sourceIndex, List> nodesToVerify) {
Node node = data.get(sourceIndex, REAL_NODES).get();
Node replacement = new Node(node.virtual, new ArrayList(node.path()));
replaceNode(node, replacement, true);
nodesToVerify.add(replacement);
}
/**
* Remove the node at the specified index, firing all the required
* notifications.
*/
private void deleteAndDetachNode(int sourceIndex, List> nodesToVerify) {
Node node = data.get(sourceIndex, REAL_NODES).get();
// if it has children, replace it with a virtual copy and schedule that for verification
if(!node.isLeaf()) {
Node replacement = new Node(node.virtual, new ArrayList(node.path()));
replaceNode(node, replacement, true);
nodesToVerify.add(replacement);
// otherwise delete it directly
} else {
Node follower = node.next();
deleteNode(node);
// remove the parent if necessary in the next iteration
nodesToVerify.add(node.parent);
// also remove the follower - it may have become redundant as well
if(follower != null && follower.virtual) nodesToVerify.add(follower);
}
}
/**
* Replace the before node with the replacement node, updating the tree
* structure completely. This is used to replace a virtual node with a real
* one or vice versa.
*/
private void replaceNode(Node before, Node after, boolean virtual) {
assert(before.pathLength() == after.pathLength());
after.expanded = before.expanded;
// change parent and children
after.parent = before.parent;
for(Node child = before.firstChild(); child != null; child = child.siblingAfter) {
assert(child.parent == before);
child.parent = after;
}
// change siblings
if(before.siblingAfter != null) {
after.siblingAfter = before.siblingAfter;
after.siblingAfter.siblingBefore = after;
}
if(before.siblingBefore != null) {
after.siblingBefore = before.siblingBefore;
after.siblingBefore.siblingAfter = after;
}
// change element
after.element = before.element;
after.element.set(after);
// change virtual
setVirtual(after, virtual);
after.virtual = virtual;
// mark the original as obsolete
before.element = null;
}
/**
* Delete all virtual parents that no longer have child nodes attached.
* This method does not depend upon child nodes being properly configured.
*/
private void deleteObsoleteVirtualLeaves(List> nodesToVerify) {
deleteObsoleteLeaves:
for(Iterator> i = nodesToVerify.iterator(); i.hasNext(); ) {
Node node = i.next();
// walk up the tree, deleting nodes
while(node != null) {
// we've reached a real parent, don't delete it!
if(!node.virtual) continue deleteObsoleteLeaves;
// we've already deleted this parent, we're done
if(node.element == null) continue deleteObsoleteLeaves;
// this node now has children, don't delete it
if(!node.isLeaf()) continue deleteObsoleteLeaves;
// if this virtual node has no children, then it's obsolete and
// we can delete it right away. Afterwards, we might need to
// delete that node's parent
deleteNode(node);
node = node.parent;
}
}
}
/**
* Remove obsolete virtual parents where there's another equal parent
* earlier in the tree. This can happen when sibling nodes are united by
* the deletion of an intervening node. For example, consider the following
* tree, where lowercase values are virtual:
*
* /a
* /a/b
* /a/b/C
* /Z
* /a
* /a/b
* /a/b/D
*
*
* If the node /Z
is deleted, then the second set of
* /a
and /a/b
nodes are now redundant. In this
* case, we must delete those as well.
*
*
Because it depends upon ancestry being properly configured, this method may
* only be executed after the tree's parent and sibling nodes have been attached.
*/
private void deleteObsoleteVirtualParents(List> nodesToVerify) {
deleteObsoleteParents:
for(Iterator> i = nodesToVerify.iterator(); i.hasNext(); ) {
Node node = i.next();
// walk up the tree, deleting nodes
while(node != null) {
// we've reached a real parent, don't delete it!
if(!node.virtual) continue deleteObsoleteParents;
// we've already deleted this parent, we're done
if(node.element == null) continue deleteObsoleteParents;
// todo: come up with a test case where previous pathlength == parent pathlength,
// which will cause this to fail (slightly) because the expanded state will be destroyed
Node previous = node.previous();
if(previous == null) continue deleteObsoleteParents;
if(!isAncestorByValue(previous, node)) continue deleteObsoleteParents;
// if this virtual parent is redundant, then we can delete it
node = deleteVirtualAncestryRootDown(previous, node);
}
}
}
private Node deleteVirtualAncestryRootDown(Node previous, Node parent) {
Node replacementLastSibling = previous.ancestorWithPathLength(parent.pathLength() + 1);
assert(replacementLastSibling.siblingAfter == null);
Node replacement = replacementLastSibling.parent;
// merge expand/collapse state first
if(replacement.expanded && !parent.expanded) {
setExpanded(parent, true);
} else if(parent.expanded && !replacement.expanded) {
setExpanded(replacement, true);
}
// link the children of the two parents as siblings
Node parentFirstChild = parent.firstChild();
assert(parentFirstChild == null || parentFirstChild.siblingBefore == null);
replacementLastSibling.siblingAfter = parentFirstChild;
if(parentFirstChild != null) parentFirstChild.siblingBefore = replacementLastSibling;
// point all children at the new parent
for(Node child = parentFirstChild; child != null; child = child.siblingAfter) {
child.parent = replacement;
}
// remove the parent itself
deleteNode(parent);
// next up for potential deletion is the child of this parent
return parentFirstChild;
}
/**
* Delete the actual node, without unlinking children or unlinking siblings,
* which must be handled externally.
*/
private void deleteNode(Node node) {
// remove links to this node from siblings
node.detachSiblings();
boolean visible = node.isVisible();
if(visible) {
int viewIndex = data.indexOfNode(node.element, VISIBLE_NODES);
updates.elementDeleted(viewIndex, node.getElement());
}
data.remove(node.element);
node.element = null; // null out the element
}
/**
* @return the length of the common prefix path between the specified nodes
*/
private int commonPathLength(Node a, Node b) {
List aPath = a.path;
List bPath = b.path;
int maxCommonPathLength = Math.min(aPath.size(), bPath.size());
// walk through the paths, looking for the first difference
for(int i = 0; i < maxCommonPathLength; i++) {
if(!valuesEqual(i, aPath.get(i), bPath.get(i))) {
return i;
}
}
return maxCommonPathLength;
}
/**
* @return true if the path of possibleAncestor is a proper prefix of
* the path of this node.
*/
private boolean isAncestorByValue(Node child, Node possibleAncestor) {
if(possibleAncestor == null) return true;
List possibleAncestorPath = possibleAncestor.path;
// this is too long a path to be an ancestor's
if(possibleAncestorPath.size() >= child.path.size()) return false;
// make sure the whole trail of the ancestor is common with our trail
for(int d = possibleAncestorPath.size() - 1; d >= 0; d--) {
E possibleAncestorPathElement = possibleAncestorPath.get(d);
E pathElement = child.path.get(d);
if(!valuesEqual(d, possibleAncestorPathElement, pathElement)) return false;
}
return true;
}
/**
* Compare two path elements for equality. The path elements will always
* have the same depth.
*
* @param depth the index of the value in the element's path. For example
* if the path is /Users/jessewilson/Desktop/yarbo.mp4
,
* then depth 3 corresponds to the element yarmo.mp4
.
*/
private boolean valuesEqual(int depth, E a, E b) {
Comparator super E> comparator = format.getComparator(depth);
if (comparator != null) {
return comparator.compare(a, b) == 0;
} else {
return GlazedListsImpl.equal(a, b);
}
}
/** {@inheritDoc} */
@Override
public void dispose() {
source.removeListEventListener(this);
initializationData.dispose();
}
/**
* Provide the expand/collapse state of nodes.
*
* This interface will be consulted when nodes are inserted. Whenever
* the expanded/collapsed state of an element is changed, this provider
* is notified. It is not strictly necessary for implementors to record the
* expand/collapsed state of all nodes, since {@link TreeList} caches
* node state internally.
*/
public interface ExpansionModel {
/**
* Determine the specified element's initial expand/collapse state.
*
* @param element the newly inserted (or unfiltered etc.) value
* @param path the tree path of the element, from root to the value.
* @return true
if the specified node's children shall be
* visible, or false
if they should be hidden.
*/
boolean isExpanded(E element, List path);
/**
* Notifies this handler that the specified element's expand/collapse
* state has changed.
*/
void setExpanded(E element, List path, boolean expanded);
}
/**
* The default state provider, that starts all nodes off as expanded.
*/
private static class DefaultExpansionModel implements ExpansionModel {
private boolean expanded;
public DefaultExpansionModel(boolean expanded) {
this.expanded = expanded;
}
@Override
public boolean isExpanded(E element, List path) {
return expanded;
}
@Override
public void setExpanded(E element, List path, boolean expanded) {
// do nothing
}
}
/**
* Prepare the state of the node and insert it into the datastore. It will
* still be necessary to attach parent and sibling nodes.
*/
private void addNode(Node node, byte nodeColor, int realIndex) {
node.element = data.add(realIndex, ALL_NODES, nodeColor, node, 1);
}
/**
* A node state provider that clones the state from another subtree. This
* is useful when a node tree gets split.
*/
private static class CloneStateNewNodeStateModel implements ExpansionModel {
private boolean[] expandedStateByDepth;
/**
* Save the expanded state of nodes. This is necessary because the state of
* {@code nodeToPrototype} is subject to change.
*/
public CloneStateNewNodeStateModel(Node nodeToPrototype) {
expandedStateByDepth = new boolean[nodeToPrototype.pathLength()];
// walk up the tree to the root, saving ancestor's expanded state
Node nodeWithDepthD = nodeToPrototype;
for(int d = expandedStateByDepth.length - 1; d >= 0; d--) {
if(nodeWithDepthD != null) {
expandedStateByDepth[d] = nodeWithDepthD.expanded;
nodeWithDepthD = nodeWithDepthD.parent;
} else {
expandedStateByDepth[d] = true;
}
}
}
@Override
public boolean isExpanded(E element, List path) {
return expandedStateByDepth[path.size() - 1];
}
@Override
public void setExpanded(E element, List path, boolean expanded) {
// do nothing
}
}
/**
* Define the tree structure of an node by expressing the path from the
* element itself to the tree's root.
*/
public interface Format {
/**
* Populate path with a list describing the path from a root node to
* this element. Upon returning, the list must have size >= 1, where
* the provided element identical to the list's last element.
*
* @param path a list that the implementor shall add their path
* elements to via path.add()
. This may be a non-empty
* List and it is an error to call any method other than add().
*/
public void getPath(List path, E element);
/**
* Whether an element can have children.
*
* @return true
if this element can have child elements,
* or false
if it is always a leaf node.
*/
public boolean allowsChildren(E element);
/**
* Returns the comparator used to order path elements of the specified
* depth. If enforcing order at this level is not intended, this method
* should return null
.
*/
public Comparator super E> getComparator(int depth);
}
/**
* Convert the specified {@link Comparator} into a {@link Comparator} for
* nodes of type E.
*/
private static NodeComparator comparatorToNodeComparator(Format format) {
return new NodeComparator(format);
}
static class NodeComparator implements Comparator> {
private final Format format;
public NodeComparator(Format format) {
if(format == null) throw new IllegalArgumentException();
this.format = format;
}
@Override
public int compare(Node a, Node b) {
int aPathLength = a.path.size();
int bPathLength = b.path.size();
// get the effective length, everything but the leaf nodes in the path
boolean aAllowsChildren = a.virtual || format.allowsChildren(a.path.get(aPathLength - 1));
boolean bAllowsChildren = b.virtual || format.allowsChildren(b.path.get(bPathLength - 1));
int aEffectiveLength = aPathLength + (aAllowsChildren ? 0 : -1);
int bEffectiveLength = bPathLength + (bAllowsChildren ? 0 : -1);
// compare by value first
for(int d = 0; d < aEffectiveLength && d < bEffectiveLength; d++) {
Comparator super E> comparator = format.getComparator(d);
if (comparator == null) return 0;
int result = comparator.compare(a.path.get(d), b.path.get(d));
if(result != 0) return result;
}
// and path length second
return aEffectiveLength - bEffectiveLength;
}
}
/**
* A private function used to convert list elements into tree paths.
*/
private static class ElementToTreeNodeFunction implements FunctionList.AdvancedFunction> {
private final Format format;
private final ExpansionModel expansionModel;
public ElementToTreeNodeFunction(Format format, ExpansionModel expansionModel) {
this.format = format;
this.expansionModel = expansionModel;
}
@Override
public Node evaluate(E sourceValue) {
// populate the path using the working path as a temporary variable
List path = new ArrayList();
format.getPath(path, sourceValue);
Node result = new Node(false, path);
result.expanded = expansionModel.isExpanded(sourceValue, path);
return result;
}
@Override
public Node reevaluate(E sourceValue, Node transformedValue) {
assert(!transformedValue.virtual);
Node result = evaluate(sourceValue);
result.expanded = transformedValue.expanded;
return result;
}
@Override
public void dispose(E sourceValue, Node transformedValue) {
// do nothing
}
}
private static class NoOpFunction implements FunctionList.Function {
@Override
public Object evaluate(Object sourceValue) {
return sourceValue;
}
}
/**
* A node in the display tree.
*/
public static final class Node {
private final List path;
/** true if this node isn't in the source list */
private boolean virtual;
/** true if this node's children should be visible */
private boolean expanded;
/** the element object points back at this, for the tree's structure cache */
private Element> element;
/** the relationship of this node to others */
private Node siblingAfter;
private Node siblingBefore;
private Node parent;
/**
* Nodes are temporarily aware if they're brand new nodes that haven't
* yet appeared in the tree. These nodes are processed slightly
* differently than nodes that already existed in the tree.
*
* This value should always be false when a change is not in
* progress.
*/
private boolean isNewlyInserted = false;
/**
* Construct a new node.
*
* @param virtual true
if this node is initially virtual
* @param path the tree path from root to value that this node represents. It
* is an error to mutate this path once it has been provided to a node.
*/
Node(boolean virtual, List path) {
this.virtual = virtual;
this.path = path;
}
/**
* Clean up this node for insertion into the tree. It might have some
* residual state due to a previous location in the tree, in the event
* that the node was subject to a reordering event.
*/
private void resetDerivedState() {
virtual = false;
element = null;
siblingAfter = null;
siblingBefore = null;
parent = null;
}
/**
* The length of the path to this node, in nodes.
*
* @return 1 for the root node, 2 for its children, etc.
*/
private int pathLength() {
return path.size();
}
/**
* Get the List element at the end of this path.
*/
public E getElement() {
return path.get(path.size() - 1);
}
/**
* @return the path elements for this element, it is an error to modify.
*/
public List path() {
return path;
}
/**
* Create a {@link Node} that resembles the parent of this.
*/
private Node describeParent() {
int pathLength = pathLength();
// this is a root node, it has no parent
if(pathLength == 1) return null;
// return a node describing the parent path
return new Node(true, new ArrayList(path.subList(0, pathLength - 1)));
}
/** {@inheritDoc} */
@Override
public String toString() {
return path.toString();
}
/**
* @return true
if this node shows up in the output
* EventList, or false
if it's a descendent of a
* collapsed node.
*/
public boolean isVisible() {
return (element.getColor() & VISIBLE_NODES) > 0;
}
/**
* @return true
if this node is virtual and not real.
*/
boolean isVirtual() {
return virtual;
}
/**
* @return true
if this node has at least one child node,
* according to our structure cache.
*/
public boolean isLeaf() {
Node next = next();
if(next == null) return true;
return next.parent != this;
}
/**
* @return the node that follows this one, or null
if there
* is no such node.
*/
private Node next() {
// TODO(jessewilson): this check prevents us from failing when the data
// is inconstent. We don't like this check since it's broken that we need
// to do it, instead we should be throwing IllegalStateException
if(element == null) {
return null;
}
Element> next = element.next();
return (next == null) ? null : next.get();
}
/**
* @return the node that precedes this one, or null
if there
* is no such node.
*/
private Node previous() {
Element> previous = element.previous();
return (previous == null) ? null : previous.get();
}
/**
* Get the first child of this node, or null
if no
* such child exists. This is not by value, but by the
* current tree structure.
*/
private Node firstChild() {
// the first child is always the node immediately after
Node possibleChild = next();
if(possibleChild == null) return null;
if(possibleChild.parent != this) return null;
return possibleChild;
}
/**
* List all children of this node.
*/
public List> getChildren() {
List> result = new ArrayList>();
for(Node child = firstChild(); child != null; child = child.siblingAfter) {
result.add(child);
}
return result;
}
/**
* Lookup the ancestor of this node whose path length is the length
* specified. For example, the ancestor of path length 2 of the node
* /Users/jessewilson/Desktop/yarbo.mp4
is the node
* /Users/jessewilson
.
*
* If the ancestor path length is the same as this node's path length,
* then this node will be returned.
*/
private Node ancestorWithPathLength(int ancestorPathLength) {
assert(pathLength() >= ancestorPathLength);
Node ancestor = this;
while(ancestor.pathLength() > ancestorPathLength) {
ancestor = ancestor.parent;
if(ancestor == null) {
throw new IllegalStateException();
}
}
return ancestor;
}
/**
* Detach the siblings of the specified node so those siblings remain
* well-formed in absence of this node.
*/
private void detachSiblings() {
// remove myself, linked list style
if(siblingBefore != null) {
siblingBefore.siblingAfter = siblingAfter;
}
if(siblingAfter != null) {
siblingAfter.siblingBefore = siblingBefore;
}
}
/** {@inheritDoc} */
@Override
public boolean equals(Object o) {
if(this == o) return true;
final Node node = (Node) o;
return path.equals(node.path);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return path.hashCode();
}
}
/**
* Expose the visible tree as an {@link EventList>}.
*/
private class NodesList extends TransformedList> {
public NodesList() {
super(TreeList.this);
TreeList.this.addListEventListener(this);
}
@Override
protected boolean isWritable() {
return true;
}
@Override
public Node get(int index) {
return getTreeNode(index);
}
@Override
public void listChanged(ListEvent listChanges) {
updates.forwardEvent(listChanges);
}
}
/**
* Expose the entire tree (including collapsed and uncollapsed Nodes.
*/
private class AllNodesList extends AbstractList> {
@Override
public Node get(int index) {
return data.get(index, ALL_NODES).get();
}
@Override
public int size() {
return data.size(ALL_NODES);
}
}
/**
* @return true if the visibility of this node is what it's parents prescribe.
*/
private boolean isVisibilityValid(Node node) {
boolean expectedVisible = true;
for(Node ancestor = node.parent; ancestor != null; ancestor = ancestor.parent) {
if(!ancestor.expanded) {
expectedVisible = false;
break;
}
}
return node.isVisible() == expectedVisible;
}
/**
* Sanity check the entire datastructure.
*/
private boolean isValid() {
// we should have the correct number of real nodes
assert(source.size() == data.size(REAL_NODES));
// walk through the tree, validating structure and each subtree
int lastPathLengthSeen = 0;
for(int i = 0; i < data.size(ALL_NODES); i++) {
Node node = data.get(i, ALL_NODES).get();
// all nodes must have elements
assert(node.element != null);
assert(!node.isNewlyInserted);
// path lengths should only grow by one from one child to the next
assert(node.pathLength() <= lastPathLengthSeen + 1);
lastPathLengthSeen = node.pathLength();
// the node's visibility should be consistent with its parent nodes expanded state
if(!isVisibilityValid(node)) {
throw new IllegalStateException();
}
// the virtual flag should be consistent with the node color
if(node.virtual) {
assert(node.element.getColor() == HIDDEN_VIRTUAL|| node.element.getColor() == VISIBLE_VIRTUAL);
} else {
if(source.get(data.convertIndexColor(i, ALL_NODES, REAL_NODES)) != node) {
throw new IllegalStateException();
}
assert(node.element.getColor() == HIDDEN_REAL || node.element.getColor() == VISIBLE_REAL);
}
// only validate the roots, they'll validate the rest recursively
if(node.pathLength() == 1) {
validateSubtree(node);
}
}
return true;
}
private void validateSubtree(Node node) {
int index = data.indexOfNode(node.element, ALL_NODES);
int size = subtreeSize(index, false, true);
// validate all children in the subtree
Node lastChildSeen = null;
for(int i = 1; i < size; i++) {
Node descendent = data.get(index + i, ALL_NODES).get();
// if this is a direct child, validate it
if(descendent.pathLength() == node.pathLength() + 1) {
if(descendent.parent != node) {
throw new IllegalStateException();
}
assert(descendent.parent == node);
if(lastChildSeen != descendent.siblingBefore) {
throw new IllegalStateException();
}
if(lastChildSeen != null) {
if(lastChildSeen.siblingAfter != descendent) {
throw new IllegalStateException();
}
if(lastChildSeen.pathLength() != descendent.pathLength()) {
throw new IllegalStateException();
}
} else {
if(descendent.siblingBefore != null) {
throw new IllegalStateException();
}
}
lastChildSeen = descendent;
validateSubtree(descendent);
// skip grandchildren and deeper
} else if(descendent.pathLength() > node.pathLength() + 1) {
// do nothing
// we've found a parent in our subtree?
} else {
throw new IllegalStateException();
}
}
// make sure we don't have a trailing sibling
assert(lastChildSeen == null || lastChildSeen.siblingAfter == null);
}
private static class FakeElement implements Element {
@Override
public Object get() { return null; }
@Override
public void set(Object value) { }
@Override
public byte getColor() { return 0; }
@Override
public void setSorted(int sorted) { }
@Override
public int getSorted() { return 0; }
@Override
public Element next() { return null; }
@Override
public Element previous() { return null; }
}
}