package org.jabref.model.util; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiPredicate; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.jabref.model.TreeNode; /** * Merges a list of nodes into a tree. * Nodes with a common parent are added as direct children. * For example, the list { A > A1, A > A2, B } is transformed into the forest { A > A1, A2, B}. */ public class TreeCollector<T> implements Collector<T, ObservableList<T>, ObservableList<T>> { private Function<T, List<T>> getChildren; private BiConsumer<T, T> addChild; private BiPredicate<T, T> equivalence; /** * @param getChildren a function that returns a list of children of the specified node * @param addChild a function that adds the second argument as a child to the first-specified node * @param equivalence a function that tells us whether two nodes are equivalent */ private TreeCollector(Function<T, List<T>> getChildren, BiConsumer<T, T> addChild, BiPredicate<T, T> equivalence) { this.getChildren = getChildren; this.addChild = addChild; this.equivalence = equivalence; } public static <T extends TreeNode<T>> TreeCollector<T> mergeIntoTree(BiPredicate<T, T> equivalence) { return new TreeCollector<T>( TreeNode::getChildren, (parent, child) -> child.moveTo(parent), equivalence); } @Override public Supplier<ObservableList<T>> supplier() { return FXCollections::observableArrayList; } @Override public BiConsumer<ObservableList<T>, T> accumulator() { return (alreadyProcessed, newItem) -> { // Check if the node is already in the tree Optional<T> sameItemInTree = alreadyProcessed.stream() .filter(item -> equivalence.test(item, newItem)) .findFirst(); if (sameItemInTree.isPresent()) { for (T child : new ArrayList<>(getChildren.apply(newItem))) { merge(sameItemInTree.get(), child); } } else { alreadyProcessed.add(newItem); } }; } private void merge(T target, T node) { Optional<T> sameItemInTree = getChildren.apply(target).stream() .filter(item -> equivalence.test(item, node)) .findFirst(); if (sameItemInTree.isPresent()) { // We need to copy the list because the #addChild method might remove the child from its own parent for (T child : new ArrayList<>(getChildren.apply(node))) { merge(sameItemInTree.get(), child); } } else { addChild.accept(target, node); } } @Override public BinaryOperator<ObservableList<T>> combiner() { return (list1, list2) -> { for (T item : list2) { accumulator().accept(list1, item); } return list1; }; } @Override public Function<ObservableList<T>, ObservableList<T>> finisher() { return i -> i; } @Override public Set<Characteristics> characteristics() { return EnumSet.of(Characteristics.UNORDERED, Characteristics.IDENTITY_FINISH); } }