package org.jabref.model;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
/**
* Represents a node in a tree.
* <p>
* Usually, tree nodes have a value property which allows access to the value stored in the node.
* In contrast to this approach, the TreeNode<T> class is designed to be used as a base class which provides the
* tree traversing functionality via inheritance.
* <p>
* Example usage:
* private class BasicTreeNode extends TreeNode<BasicTreeNode> {
* public BasicTreeNode() {
* super(BasicTreeNode.class);
* }
* }
* <p>
* This class started out as a copy of javax.swing.tree.DefaultMutableTreeNode.
*
* @param <T> the type of the class
*/
// We use some explicit casts of the form "(T) this". The constructor ensures that this cast is valid.
@SuppressWarnings("unchecked") public abstract class TreeNode<T extends TreeNode<T>> {
/**
* Array of children, may be empty if this node has no children (but never null)
*/
private final ObservableList<T> children;
/**
* This node's parent, or null if this node has no parent
*/
private T parent;
/**
* The function which is invoked when something changed in the subtree.
*/
private Consumer<T> onDescendantChanged = t -> {
/* Do nothing */
};
/**
* Constructs a tree node without parent and no children.
*
* @param derivingClass class deriving from TreeNode<T>. It should always be "T.class".
* We need this parameter since it is hard to get this information by other means.
*/
public TreeNode(Class<T> derivingClass) {
parent = null;
children = FXCollections.observableArrayList();
if (!derivingClass.isInstance(this)) {
throw new UnsupportedOperationException("The class extending TreeNode<T> has to derive from T");
}
}
/**
* Get the path from the root node to this node.
* <p>
* The elements in the returned list represent the child index of each node in the path, starting at the root.
* If this node is the root node, the returned list has zero elements.
*
* @return a list of numbers which represent an indexed path from the root node to this node
*/
public List<Integer> getIndexedPathFromRoot() {
if (parent == null) {
return new ArrayList<>();
}
List<Integer> path = parent.getIndexedPathFromRoot();
path.add(getPositionInParent());
return path;
}
/**
* Get the descendant of this node as indicated by the indexedPath.
* <p>
* If the path could not be traversed completely (i.e. one of the child indices did not exist),
* an empty Optional will be returned.
*
* @param indexedPath sequence of child indices that describe a path from this node to one of its descendants.
* Be aware that if indexedPath was obtained by getIndexedPathFromRoot(), this node should
* usually be the root node.
* @return descendant found by evaluating indexedPath
*/
public Optional<T> getDescendant(List<Integer> indexedPath) {
T cursor = (T) this;
for (int index : indexedPath) {
Optional<T> child = cursor.getChildAt(index);
if (child.isPresent()) {
cursor = child.get();
} else {
return Optional.empty();
}
}
return Optional.of(cursor);
}
/**
* Get the child index of this node in its parent.
* <p>
* If this node is a root, then an UnsupportedOperationException is thrown.
* Use the isRoot method to check for this case.
*
* @return the child index of this node in its parent
*/
public int getPositionInParent() {
return getParent().orElseThrow(() -> new UnsupportedOperationException("Roots have no position in parent"))
.getIndexOfChild((T) this).get();
}
/**
* Gets the index of the specified child in this node's child list.
* <p>
* If the specified node is not a child of this node, returns an empty Optional.
* This method performs a linear search and is O(n) where n is the number of children.
*
* @param childNode the node to search for among this node's children
* @return an integer giving the index of the node in this node's child list
* or an empty Optional if the specified node is a not a child of this node
* @throws NullPointerException if childNode is null
*/
public Optional<Integer> getIndexOfChild(T childNode) {
Objects.requireNonNull(childNode);
int index = children.indexOf(childNode);
if (index == -1) {
return Optional.empty();
} else {
return Optional.of(index);
}
}
/**
* Gets the number of levels above this node, i.e. the distance from the root to this node.
* <p>
* If this node is the root, returns 0.
*
* @return an int giving the number of levels above this node
*/
public int getLevel() {
if (parent == null) {
return 0;
}
return parent.getLevel() + 1;
}
/**
* Returns the number of children of this node.
*
* @return an int giving the number of children of this node
*/
public int getNumberOfChildren() {
return children.size();
}
/**
* Removes this node from its parent and makes it a child of the specified node
* by adding it to the end of children list.
* In this way the whole subtree based at this node is moved to the given node.
*
* @param target the new parent
* @throws NullPointerException if target is null
* @throws ArrayIndexOutOfBoundsException if targetIndex is out of bounds
* @throws UnsupportedOperationException if target is an descendant of this node
*/
public void moveTo(T target) {
Objects.requireNonNull(target);
Optional<T> oldParent = getParent();
if (oldParent.isPresent() && (oldParent.get() == target)) {
this.moveTo(target, target.getNumberOfChildren() - 1);
} else {
this.moveTo(target, target.getNumberOfChildren());
}
}
/**
* Returns the path from the root, to get to this node. The last element in the path is this node.
*
* @return a list of nodes giving the path, where the first element in the path is the root
* and the last element is this node.
*/
public List<T> getPathFromRoot() {
if (parent == null) {
List<T> pathToMe = new ArrayList<>();
pathToMe.add((T) this);
return pathToMe;
}
List<T> path = parent.getPathFromRoot();
path.add((T) this);
return path;
}
/**
* Returns the next sibling of this node in the parent's children list.
* Returns an empty Optional if this node has no parent or if it is the parent's last child.
* <p>
* This method performs a linear search that is O(n) where n is the number of children.
* To traverse the entire children collection, use the parent's getChildren() instead.
*
* @return the sibling of this node that immediately follows this node
* @see #getChildren
*/
public Optional<T> getNextSibling() {
return getRelativeSibling(+1);
}
/**
* Returns the previous sibling of this node in the parent's children list.
* Returns an empty Optional if this node has no parent or is the parent's first child.
* <p>
* This method performs a linear search that is O(n) where n is the number of children.
*
* @return the sibling of this node that immediately precedes this node
* @see #getChildren
*/
public Optional<T> getPreviousSibling() {
return getRelativeSibling(-1);
}
/**
* Returns the sibling which is shiftIndex away from this node.
*/
private Optional<T> getRelativeSibling(int shiftIndex) {
if (parent == null) {
return Optional.empty();
} else {
int indexInParent = getPositionInParent();
int indexTarget = indexInParent + shiftIndex;
if (parent.childIndexExists(indexTarget)) {
return parent.getChildAt(indexTarget);
} else {
return Optional.empty();
}
}
}
/**
* Returns this node's parent or an empty Optional if this node has no parent.
*
* @return this node's parent T, or an empty Optional if this node has no parent
*/
public Optional<T> getParent() {
return Optional.ofNullable(parent);
}
/**
* Sets the parent node of this node.
* <p>
* This method does not add this node to the children collection of the new parent nor does it remove this node
* from the old parent. You should probably call moveTo or remove to change the tree.
*
* @param parent the new parent
*/
protected void setParent(T parent) {
this.parent = parent;
}
/**
* Returns the child at the specified index in this node's children collection.
*
* @param index an index into this node's children collection
* @return the node in this node's children collection at the specified index,
* or an empty Optional if the index does not point to a child
*/
public Optional<T> getChildAt(int index) {
return childIndexExists(index) ? Optional.of(children.get(index)) : Optional.empty();
}
/**
* Returns whether the specified index is a valid index for a child.
*
* @param index the index to be tested
* @return returns true when index is at least 0 and less then the count of children
*/
protected boolean childIndexExists(int index) {
return (index >= 0) && (index < children.size());
}
/**
* Returns true if this node is the root of the tree.
* The root is the only node in the tree with an empty parent; every tree has exactly one root.
*
* @return true if this node is the root of its tree
*/
public boolean isRoot() {
return parent == null;
}
/**
* Returns true if this node is an ancestor of the given node.
* <p>
* A node is considered an ancestor of itself.
*
* @param anotherNode node to test
* @return true if anotherNode is a descendant of this node
* @throws NullPointerException if anotherNode is null
* @see #isNodeDescendant
*/
public boolean isAncestorOf(T anotherNode) {
Objects.requireNonNull(anotherNode);
if (anotherNode == this) {
return true;
} else {
for (T child : children) {
if (child.isAncestorOf(anotherNode)) {
return true;
}
}
return false;
}
}
/**
* Returns the root of the tree that contains this node. The root is the ancestor with an empty parent.
* Thus a node without a parent is considered its own root.
*
* @return the root of the tree that contains this node
*/
public T getRoot() {
if (parent == null) {
return (T) this;
} else {
return parent.getRoot();
}
}
/**
* Returns true if this node has no children.
*
* @return true if this node has no children
*/
public boolean isLeaf() {
return (getNumberOfChildren() == 0);
}
/**
* Removes the subtree rooted at this node from the tree, giving this node an empty parent.
* Does nothing if this node is the root of it tree.
*/
public void removeFromParent() {
if (parent != null) {
parent.removeChild((T) this);
}
}
/**
* Removes all of this node's children, setting their parents to empty.
* If this node has no children, this method does nothing.
*/
public void removeAllChildren() {
while (getNumberOfChildren() > 0) {
removeChild(0);
}
}
/**
* Returns this node's first child if it exists (otherwise returns an empty Optional).
*
* @return the first child of this node
*/
public Optional<T> getFirstChild() {
return getChildAt(0);
}
/**
* Returns this node's last child if it exists (otherwise returns an empty Optional).
*
* @return the last child of this node
*/
public Optional<T> getLastChild() {
return getChildAt(children.size() - 1);
}
/**
* Returns true if anotherNode is a descendant of this node
* -- if it is this node, one of this node's children, or a descendant of one of this node's children.
* Note that a node is considered a descendant of itself.
* <p>
* If anotherNode is null, an exception is thrown.
*
* @param anotherNode node to test as descendant of this node
* @return true if this node is an ancestor of anotherNode
* @see #isAncestorOf
*/
public boolean isNodeDescendant(T anotherNode) {
Objects.requireNonNull(anotherNode);
return this.isAncestorOf(anotherNode);
}
/**
* Gets a forward-order list of this node's children.
* <p>
* The returned list is unmodifiable - use the add and remove methods to modify the nodes children.
* However, changing the nodes children (for example by calling moveTo) is reflected in a change of
* the list returned by getChildren. In other words, getChildren provides a read-only view on the children but
* not a copy.
*
* @return a list of this node's children
*/
public ObservableList<T> getChildren() {
return FXCollections.unmodifiableObservableList(children);
}
/**
* Removes the given child from this node's child list, giving it an empty parent.
*
* @param child a child of this node to remove
*/
public void removeChild(T child) {
Objects.requireNonNull(child);
children.remove(child);
child.setParent(null);
notifyAboutDescendantChange((T)this);
}
/**
* Removes the child at the specified index from this node's children and sets that node's parent to empty.
* <p>
* Does nothing if the index does not point to a child.
*
* @param childIndex the index in this node's child array of the child to remove
*/
public void removeChild(int childIndex) {
Optional<T> child = getChildAt(childIndex);
if (child.isPresent()) {
children.remove(childIndex);
child.get().setParent(null);
}
notifyAboutDescendantChange((T)this);
}
/**
* Adds the node at the end the children collection. Also sets the parent of the given node to this node.
* The given node is not allowed to already be in a tree (i.e. it has to have no parent).
*
* @param child the node to add
* @return the child node
*/
public T addChild(T child) {
return addChild(child, children.size());
}
/**
* Adds the node at the given position in the children collection. Also sets the parent of the given node to this node.
* The given node is not allowed to already be in a tree (i.e. it has to have no parent).
*
* @param child the node to add
* @param index the position where the node should be added
* @return the child node
* @throws IndexOutOfBoundsException if the index is out of range
*/
public T addChild(T child, int index) {
Objects.requireNonNull(child);
if (child.getParent().isPresent()) {
throw new UnsupportedOperationException("Cannot add a node which already has a parent, use moveTo instead");
}
child.setParent((T) this);
children.add(index, child);
notifyAboutDescendantChange((T)this);
return child;
}
/**
* Removes all children from this node and makes them a child of the specified node
* by adding it to the specified position in the children list.
*
* @param target the new parent
* @param targetIndex the position where the children should be inserted
* @throws NullPointerException if target is null
* @throws ArrayIndexOutOfBoundsException if targetIndex is out of bounds
* @throws UnsupportedOperationException if target is an descendant of one of the children of this node
*/
public void moveAllChildrenTo(T target, int targetIndex) {
while (getNumberOfChildren() > 0) {
getLastChild().get().moveTo(target, targetIndex);
}
}
/**
* Sorts the list of children according to the order induced by the specified {@link Comparator}.
* <p>
* All children must be mutually comparable using the specified comparator
* (that is, {@code c.compare(e1, e2)} must not throw a {@code ClassCastException}
* for any children {@code e1} and {@code e2} in the list).
*
* @param comparator the comparator used to compare the child nodes
* @param recursive if true the whole subtree is sorted
* @throws NullPointerException if the comparator is null
*/
public void sortChildren(Comparator<? super T> comparator, boolean recursive) {
Objects.requireNonNull(comparator);
if (this.isLeaf()) {
return; // nothing to sort
}
int j = getNumberOfChildren() - 1;
int lastModified;
while (j > 0) {
lastModified = j + 1;
j = -1;
for (int i = 1; i < lastModified; ++i) {
T child1 = getChildAt(i - 1).get();
T child2 = getChildAt(i).get();
if (comparator.compare(child1, child2) > 0) {
child1.moveTo((T) this, i);
j = i;
}
}
}
if (recursive) {
for (T child : getChildren()) {
child.sortChildren(comparator, true);
}
}
}
/**
* Removes this node from its parent and makes it a child of the specified node
* by adding it to the specified position in the children list.
* In this way the whole subtree based at this node is moved to the given node.
*
* @param target the new parent
* @param targetIndex the position where the children should be inserted
* @throws NullPointerException if target is null
* @throws ArrayIndexOutOfBoundsException if targetIndex is out of bounds
* @throws UnsupportedOperationException if target is an descendant of this node
*/
public void moveTo(T target, int targetIndex) {
Objects.requireNonNull(target);
// Check that the target node is not an ancestor of this node, because this would create loops in the tree
if (this.isAncestorOf(target)) {
throw new UnsupportedOperationException("the target cannot be a descendant of this node");
}
// Remove from previous parent
Optional<T> oldParent = getParent();
if (oldParent.isPresent()) {
oldParent.get().removeChild((T) this);
}
// Add as child
target.addChild((T) this, targetIndex);
}
/**
* Creates a deep copy of this node and all of its children.
*
* @return a deep copy of the subtree
*/
public T copySubtree() {
T copy = copyNode();
for (T child : getChildren()) {
child.copySubtree().moveTo(copy);
}
return copy;
}
/**
* Creates a copy of this node, completely separated from the tree (i.e. no children and no parent)
*
* @return a deep copy of this node
*/
public abstract T copyNode();
/**
* Adds the given function to the list of subscribers which are notified when something changes in the subtree.
*
* The following events are supported (the text in parentheses specifies which node is passed as the source):
* - addChild (new parent)
* - removeChild (old parent)
* - move (old parent and new parent)
* @param subscriber function to be invoked upon a change
*/
public void subscribeToDescendantChanged(Consumer<T> subscriber) {
onDescendantChanged = onDescendantChanged.andThen(subscriber);
}
/**
* Helper method which notifies all subscribers about a change in the subtree and bubbles the event to all parents.
* @param source the node which changed
*/
protected void notifyAboutDescendantChange(T source) {
onDescendantChanged.accept(source);
if (!isRoot()) {
parent.notifyAboutDescendantChange(source);
}
}
}