com.almworks.jira.structure.api.util.RowTree Maven / Gradle / Ivy
Show all versions of structure-api Show documentation
package com.almworks.jira.structure.api.util;
import com.almworks.integers.*;
import com.almworks.jira.structure.api.forest.raw.ArrayForest;
import com.almworks.jira.structure.api.forest.raw.Forest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.LongPredicate;
/**
* RowTree is another representation of the Forest concept. It is implemented as a tree structure and is
* an alternative to ArrayForest when it comes to manipulating the forest.
*
* RowTree is far more heavy on memory and GC - each row is a separate object + 52 bytes of payload. However,
* it is very fast with the modification operations.
*
* Suggested use:
*
* - Do not use for long-term forest storage!
* - Do not use when ArrayForest is sufficient for the modifications you need.
* - Do not use when you can arrange the modifications you need in a bulk array-creation fashion.
* - DO use as a temporary object when you need a (potentially big) number of random changes with a forest and
* none of the above applies.
*
*
* Additional concepts and terms:
*
* - Super-root is the "hidden" node that all root nodes are children of.
* - There's no concept of depth! Depth can be calculated by going up the parent chain, if needed. But it
* shouldn't be needed.
* - Flags field lets you store temporary bitmask data in each node to help your algorithms. Semantics is
* up to you.
*
*
* Note that this class does not check invariants (like uniqueness of row IDs).
*
* Not thread-safe.
*
* This method does not and should not implement {@code Forest} because it does not provide indexed access.
*
* @see RowTree.Node
* @see IndexedRowTree
*/
public class RowTree {
private final Node mySuperRoot = new Node(0, 0);
public Node getSuperRoot() {
return mySuperRoot;
}
@Override
public String toString() {
return mySuperRoot.toFullString();
}
/**
* Appends forest to the end of the tree. Forest roots also become roots in the RowTree.
* @param forest the forest
*/
public void appendForest(@NotNull Forest forest) {
appendForest(forest, mySuperRoot, mySuperRoot.getLastChild(), -1, 0, (LongPredicate) null);
}
/**
* Appends forest at the specified place.
* @param forest the forest
* @param underNode the would-be parent
* @param afterNode the would-be preceding sibling, or null if the inserted nodes should come first under {@code underNode}
*/
public void appendForest(@NotNull Forest forest, @NotNull Node underNode, @Nullable Node afterNode) {
appendForest(forest, underNode, afterNode, -1, 0, (LongPredicate) null);
}
/**
* Generic utility method for adding the forest to the RowTree, with parameters for optimization.
*
* @param forest the source forest
* @param parentNode the would-be parent
* @param afterNode the would-be preceding sibling, or null if the inserted nodes should come first under {@code underNode}
* @param parentIndex the index of the source row in {@code forest} - only children of of that row and their sub-trees are
* added (the row itself is not added); use -1 to insert the whole forest
* @param flags initial flags for all created nodes
* @param rowFilter when not null, each node is tested with this predicate; failed nodes and their sub-trees are skipped
* (even if some row in the sub-tree passes the filter)
* @return the index in the forest where iteration stopped (subtreeEnd for the parentNode)
*/
public int appendForest(@NotNull Forest forest, @NotNull Node parentNode, @Nullable Node afterNode,
int parentIndex, int flags, @Nullable LongPredicate rowFilter)
{
assert afterNode == null || afterNode.getParent() == parentNode : parentNode + " " + afterNode;
// when we go higher than minDepth, stop iteration
int minDepth = parentIndex < 0 ? 0 : forest.getDepth(parentIndex) + 1;
// we will walk the tree with currentNode/currentDepth
Node currentNode = afterNode;
int currentDepth = minDepth;
if (currentNode == null) {
currentNode = parentNode;
currentDepth--;
}
int size = forest.size();
int i = parentIndex + 1;
while (i < size) {
int depth = forest.getDepth(i);
if (depth < minDepth) {
// end of children and their subtrees
break;
}
long rowId = forest.getRow(i);
if (rowFilter != null && !rowFilter.test(rowId)) {
// skip subtree
i = forest.getSubtreeEnd(i);
} else {
Node node = createNode(rowId, flags);
if (currentDepth < depth) {
// stepping down the depth
assert depth == currentDepth + 1 : "forest invariant broken @" + i + ": depth " + currentDepth + " => " + depth;
currentNode = currentNode.insertChild(node);
currentDepth++;
} else {
while (currentDepth > depth) {
// stepping up the depth
currentNode = currentNode.getParent();
currentDepth--;
}
currentNode = currentNode.appendSibling(node);
}
i++;
}
}
return i;
}
/**
* Adds a node to the tree at the specified position.
*
* @param rowId row ID
* @param parentNode the would-be parent
* @param afterNode the would-be preceding sibling, or null if the inserted nodes should come first under {@code underNode}
* @param flags initial flags for all created nodes
*
* @return inserted node
*/
public Node insertNode(long rowId, int flags, Node parentNode, Node afterNode) {
return insertNaturalizedNode(createNode(rowId, flags), parentNode, afterNode);
}
/**
* Moves node within a tree
*
* @param node node to move
* @param parentNode the would-be parent
* @param afterNode the would-be preceding sibling, or null if the inserted nodes should come first under {@code underNode}
*
* @return node
*/
public Node moveNode(Node node, Node parentNode, Node afterNode) {
return insertNaturalizedNode(node.pluck(), parentNode, afterNode);
}
/**
* Removes node and its sub-tree from the tree.
*
* @param node node
*/
public void remove(Node node) {
if (node == mySuperRoot) throw new IllegalArgumentException("cannot remove super-root");
node.pluck();
forgetNode(node);
}
/**
* Creates {@link Forest} representing this RowTree.
*
* @return ArrayForest
*/
public ArrayForest toForest() {
LongArray rows = new LongArray();
IntArray depths = new IntArray();
mySuperRoot.writeTo(rows, depths, -1);
return new ArrayForest(rows, depths, true);
}
/**
* Called always to create a new node for insertion in this tree.
* Intended to be overridden with additional checks and maintaining state.
*/
protected Node createNode(long rowId, int flags) {
return new Node(rowId, flags);
}
/**
* Called always when a node is removed (with its sub-nodes). Not called for sub-nodes explicitly.
* Intended to be overridden with additional checks and maintaining state.
*/
protected void forgetNode(Node node) {
}
/**
* "Naturalized" means that the node has been created with createNode() in this tree and was not yet
* forgotten with forgetNode().
*/
private Node insertNaturalizedNode(Node node, Node parentNode, Node afterChild) {
assert afterChild == null || afterChild.getParent() == parentNode : parentNode + " " + afterChild;
if (afterChild != null) {
afterChild.appendSibling(node);
} else {
parentNode.insertChild(node);
}
return node;
}
/**
* Represents a single node or a {@link RowTree}. You can traverse the tree up/down/left/right from a single node.
*
* A node may be "unplugged", in which case its {@link #getParent()} is null. (Root nodes have RowTree's super-root as
* the parent.)
*
* Public methods of {@code Node} are provided for traveral. All modifications should be done through {@code RowTree}.
*/
public static class Node {
private final long myRowId;
private Node myParent;
private Node myFirstChild;
private Node myLastChild;
private Node myNextSibling;
private Node myPrevSibling;
// user-specific flags
private int myFlags;
private Node(long rowId, int flags) {
myRowId = rowId;
myFlags = flags;
}
@Override
public String toString() {
return myFlags == 0 ? String.valueOf(myRowId) : myRowId + "#" + myFlags;
}
/**
* @return lisp-style full printout
*/
public String toFullString() {
return append(new StringBuilder()).toString();
}
public StringBuilder append(StringBuilder sb) {
if (myRowId != 0) sb.append(myRowId);
if (myFirstChild != null) {
sb.append('(');
for (Node n = myFirstChild; n != null; n = n.myNextSibling) {
if (n != myFirstChild) sb.append(',');
n.append(sb);
}
sb.append(')');
}
return sb;
}
public long getRowId() {
return myRowId;
}
/**
* Bitwise flag check.
*
* @param flagMask mask
* @return true if all flags specified in the mask are set
*/
public boolean hasFlags(int flagMask) {
return (myFlags & flagMask) == flagMask;
}
/**
* Sets flags by mask.
*
* @param flagMask mask
*/
public void addFlags(int flagMask) {
myFlags |= flagMask;
}
public Node getParent() {
assert myParent != null;
return myParent;
}
public Node getFirstChild() {
return myFirstChild;
}
public Node getLastChild() {
return myLastChild;
}
public Node getNextSibling() {
return myNextSibling;
}
public Node getPrevSibling() {
return myPrevSibling;
}
/**
* Dumps the sub-tree rooted at this node as parallel lists of rows and depths, to be later used
* as parameters for {@link ArrayForest}.
*
* @param rows rows recipient
* @param depths depths recipient
* @param depth starting depth for this node; if -1, this is a special case: this node is a super-root and
* must not write out its row ID (which is 0), but should proceed writing out children.
*/
public void writeTo(LongCollector rows, IntCollector depths, int depth) {
if (depth >= 0) {
assert myRowId != 0;
rows.add(myRowId);
depths.add(depth);
}
for (Node n = myFirstChild; n != null; n = n.myNextSibling) {
n.writeTo(rows, depths, depth + 1);
}
}
/**
* Retrieves all direct children of the node
*
* @param collector recipient for row IDs
*/
public void collectDirectChildren(LongCollector collector) {
for (Node n = myFirstChild; n != null; n = n.myNextSibling) {
collector.add(n.getRowId());
}
}
/**
* Starting with this node and going into its sub-tree, tries to find the first node that has the specified
* flags set. Walks in LNR depth-first order.
*
* @param flags flags to look for
*
* @return node with the flags or null if not found
*/
public Node findFirstWithFlags(int flags) {
if (hasFlags(flags)) return this;
for (Node n = myFirstChild; n != null; n = n.myNextSibling) {
Node r = n.findFirstWithFlags(flags);
if (r != null) return r;
}
return null;
}
// inserts node as the first child of this node
private Node insertChild(Node node) {
assert node.myParent == null;
assert node.myNextSibling == null;
assert node.myPrevSibling == null;
if (myFirstChild == null) {
assert myLastChild == null;
myLastChild = node;
} else {
node.myNextSibling = myFirstChild;
myFirstChild.myPrevSibling = node;
}
myFirstChild = node;
node.myParent = this;
return node;
}
// inserts node as the next sibling of this node
private Node appendSibling(Node node) {
assert node.myParent == null;
assert node.myNextSibling == null;
assert node.myPrevSibling == null;
if (myNextSibling == null && myParent != null) {
myParent.myLastChild = node;
}
node.myNextSibling = myNextSibling;
if (myNextSibling != null) {
myNextSibling.myPrevSibling = node;
}
myNextSibling = node;
node.myPrevSibling = this;
node.myParent = myParent;
return node;
}
/**
* Removes node (and its sub-tree) from the RowTree. Note that unless the node is reinserted in the same
* transaction, you should call {@link RowTree#forgetNode}.
*
* @return this node in "unplugged" state
*/
private Node pluck() {
if (myPrevSibling != null) {
myPrevSibling.myNextSibling = myNextSibling;
} else {
myParent.myFirstChild = myNextSibling;
}
if (myNextSibling != null) {
myNextSibling.myPrevSibling = myPrevSibling;
} else {
myParent.myLastChild = myPrevSibling;
}
myPrevSibling = null;
myNextSibling = null;
myParent = null;
return this;
}
}
}