diff --git a/platform/platform-impl/src/com/intellij/ui/tree/AbstractTreeWalker.java b/platform/platform-impl/src/com/intellij/ui/tree/AbstractTreeWalker.java new file mode 100644 index 000000000000..7d603501f13a --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/tree/AbstractTreeWalker.java @@ -0,0 +1,208 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ui.tree; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.AsyncPromise; +import org.jetbrains.concurrency.Promise; + +import javax.swing.tree.TreePath; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +abstract class AbstractTreeWalker { + private enum State {STARTED, REQUESTED, PAUSED, FINISHED, FAILED} + + private final AtomicReference state = new AtomicReference<>(); + private final AsyncPromise promise = new AsyncPromise<>(); + private final ArrayDeque> stack = new ArrayDeque<>(); + private final Function converter; + private final TreeVisitor visitor; + private volatile TreePath current; + + /** + * Creates a new tree walker with the specified tree visitor. + * + * @param visitor an object that controls visiting a tree structure + */ + public AbstractTreeWalker(@NotNull TreeVisitor visitor) { + this(visitor, node -> node); + } + + /** + * Creates a new tree walker with the specified node converter, + * which allows to generate a tree path expected by the given tree visitor. + * + * @param visitor an object that controls visiting a tree structure + * @param converter a node converter for the path components + */ + public AbstractTreeWalker(@NotNull TreeVisitor visitor, Function converter) { + this.converter = converter; + this.visitor = visitor; + } + + /** + * Returns a list of child nodes for the specified node. + * This method is called by the walker only if the visitor + * returned the {@link TreeVisitor.Action#CONTINUE CONTINUE} action. + * The walker will be paused if it returns {@code null}. + * To continue user should call the {@link #setChildren} method. + * + * @param node a node in a tree structure + * @return children for the specified node or {@code null} if children will be set later + */ + protected abstract Collection getChildren(@NotNull N node); + + /** + * Sets the children, awaited by the walker, and continues to traverse a tree structure. + * + * @param children a list of child nodes for the node specified in the {@link #getChildren} method + * @throws IllegalStateException if it is called in unexpected state + */ + public void setChildren(Collection children) { + boolean paused = state.compareAndSet(State.PAUSED, State.STARTED); + if (!paused && !state.compareAndSet(State.REQUESTED, State.STARTED)) throw new IllegalStateException(); + stack.push(children == null ? new ArrayDeque<>() : new ArrayDeque<>(children)); + if (paused) processNextPath(); + } + + /** + * @return a promise that will be resolved when visiting is finished + */ + @NotNull + public Promise promise() { + return promise; + } + + /** + * Stops visiting a tree structure by specifying a cause. + */ + public void setError(@NotNull Throwable error) { + state.set(State.FAILED); + promise.setError(error); + } + + /** + * Starts visiting a tree structure from the specified root node. + * + * @param node a tree root or {@code null} if nothing to traverse + */ + public void start(N node) { + start(null, node); + } + + /** + * Starts visiting a tree structure from the specified node. + * + * @param parent a parent tree path or {@code null} for a root node + * @param node a tree node or {@code null} if nothing to traverse + */ + public void start(TreePath parent, N node) { + TreePath result = null; + if (node != null) { + try { + Object component = converter.apply(node); + TreePath path = parent != null + ? parent.pathByAddingChild(component) + : new TreePath(component); + + switch (visitor.accept(path)) { + case CONTINUE: + update(null, State.REQUESTED); + if (processChildren(path, node)) processNextPath(); + return; + case INTERRUPT: + result = path; + break; + case SKIP_CHILDREN: + break; + case SKIP_SIBLINGS: + break; + } + } + catch (Exception error) { + setError(error); + } + } + update(null, State.FINISHED); + promise.setResult(result); + } + + /** + * @param path a path to the specified node + * @param node a node to get children to process + * @return {@code false} if the walker should be pause + */ + private boolean processChildren(@NotNull TreePath path, @NotNull N node) { + current = path; + Collection children = getChildren(node); + if (children == null) return !state.compareAndSet(State.REQUESTED, State.PAUSED); + update(State.REQUESTED, State.STARTED); + stack.push(new ArrayDeque<>(children)); + return true; + } + + private void processNextPath() { + try { + while (State.STARTED == state.get()) { + ArrayDeque siblings = stack.peek(); + if (siblings == null) { + update(State.STARTED, State.FINISHED); + current = null; + promise.setResult(null); + return; // nothing to process + } + N node = siblings.poll(); + if (node == null) { + TreePath path = current; + if (path == null) throw new IllegalStateException(); + if (siblings != stack.poll()) throw new IllegalStateException(); + current = path.getParentPath(); + } + else { + Object component = converter.apply(node); + TreePath path = this.current.pathByAddingChild(component); + switch (visitor.accept(path)) { + case CONTINUE: + update(State.STARTED, State.REQUESTED); + if (processChildren(path, node)) break; + return; // stop processing and wait for setChildren + case INTERRUPT: + update(State.STARTED, State.FINISHED); + current = null; + stack.clear(); + promise.setResult(path); + return; // path is found + case SKIP_SIBLINGS: + siblings.clear(); + break; + case SKIP_CHILDREN: + break; + } + } + } + } + catch (Exception error) { + setError(error); + } + } + + private void update(State expected, State replacement) { + if (!state.compareAndSet(expected, replacement)) throw new IllegalStateException(); + } +} diff --git a/platform/platform-impl/src/com/intellij/ui/tree/TreeVisitor.java b/platform/platform-impl/src/com/intellij/ui/tree/TreeVisitor.java new file mode 100644 index 000000000000..7a1e9a6e045a --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/tree/TreeVisitor.java @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ui.tree; + +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.TreePath; +import java.util.function.Function; + +public interface TreeVisitor { + /** + * @param path a currently visited path + * @return an action that controls visiting a tree + */ + @NotNull + Action accept(@NotNull TreePath path); + + enum Action { + /** + * Interrupt visiting a tree structure. + */ + INTERRUPT, + /** + * Continue visiting the node children. + */ + CONTINUE, + /** + * Continue without visiting the node children. + */ + SKIP_CHILDREN, + /** + * Continue without visiting the node siblings. + */ + SKIP_SIBLINGS, + } + + + abstract class Finder implements TreeVisitor { + @NotNull + @Override + public Action accept(@NotNull TreePath path) { + return found(path) ? Action.INTERRUPT : contains(path) ? Action.CONTINUE : Action.SKIP_CHILDREN; + } + + /** + * @param path a currently visited path + * @return {@code true} if the specified path is found and visiting can be interrupted + */ + protected abstract boolean found(@NotNull TreePath path); + + /** + * @param path a currently visited path + * @return {@code true} if the specified path may contain a seeking path + */ + protected abstract boolean contains(@NotNull TreePath path); + } + + + class PathFinder implements TreeVisitor { + private final Function converter; + private final TreePath path; + + public PathFinder(@NotNull TreePath path) { + this(path, object -> object); + } + + public PathFinder(@NotNull TreePath path, @NotNull Function converter) { + this.converter = converter; + this.path = path; + } + + @NotNull + @Override + public Action accept(@NotNull TreePath path) { + Object component = converter.apply(path.getLastPathComponent()); + if (component == null) return Action.SKIP_CHILDREN; + + int pathCount = path.getPathCount(); + int thisCount = this.path.getPathCount(); + if (thisCount < pathCount) return Action.SKIP_CHILDREN; + + Action action = thisCount == pathCount ? Action.INTERRUPT : Action.CONTINUE; + + TreePath value = this.path; + while (thisCount > pathCount) { + thisCount--; + value = value.getParentPath(); + if (value == null) return Action.SKIP_CHILDREN; + } + return matches(component, value.getLastPathComponent()) ? action : Action.SKIP_CHILDREN; + } + + /** + * @param pathComponent a last component of the current path + * @param thisComponent a component of the seeking path at the same level + * @return {@code true} if both components match each other + */ + protected boolean matches(@NotNull Object pathComponent, @NotNull Object thisComponent) { + return pathComponent.equals(thisComponent); + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/ui/tree/AbstractTreeWalkerTest.java b/platform/platform-tests/testSrc/com/intellij/ui/tree/AbstractTreeWalkerTest.java new file mode 100644 index 000000000000..c56154d00177 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/ui/tree/AbstractTreeWalkerTest.java @@ -0,0 +1,425 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ui.tree; + +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import java.util.ArrayList; +import java.util.Collection; + +import static com.intellij.util.ReflectionUtil.getField; +import static org.junit.Assert.assertEquals; + +public class AbstractTreeWalkerTest { + private final static boolean PRINT = false; + + @Test + public void testInterrupt() { + Node root = createRoot(); + test(root, 1, path -> TreeVisitor.Action.INTERRUPT, root); + test(null, 0, path -> TreeVisitor.Action.INTERRUPT); + } + + @Test + public void testContinue() { + test(createRoot(), 21, path -> TreeVisitor.Action.CONTINUE); + test(null, 0, path -> TreeVisitor.Action.CONTINUE); + } + + @Test + public void testSkipSiblings() { + test(createRoot(), 1, path -> TreeVisitor.Action.SKIP_SIBLINGS); + test(null, 0, path -> TreeVisitor.Action.SKIP_SIBLINGS); + } + + @Test + public void testSkipChildren() { + test(createRoot(), 1, path -> TreeVisitor.Action.SKIP_CHILDREN); + test(null, 0, path -> TreeVisitor.Action.SKIP_CHILDREN); + } + + @Test + public void testDeepVisit() { + testDeepVisit(1); + testDeepVisit(10); + testDeepVisit(100); + testDeepVisit(1000); + if (PRINT) return; + testDeepVisit(10000); + testDeepVisit(100000); + } + + private static void testDeepVisit(int count) { + Node node = new Node(count); + for (int i = 1; i < count; i++) node = new Node(count - i, node); + test(node, count, path -> TreeVisitor.Action.CONTINUE); + } + + @Test + public void testDoubleStart() { + /* + Walker walker = new Walker(path -> TreeVisitor.Action.CONTINUE, null, 0); + assertResult(walker); + */ + } + + @Test + public void testFinder() { + TreeNode root = createRoot(); + TreeNode color = root.getChildAt(0); + TreeNode digit = root.getChildAt(1); + TreeNode greek = root.getChildAt(2); + test(root, 1, createFinder(root), root); + test(root, 2, createFinder(color), root, color); + test(root, 3, createFinder(color.getChildAt(0)), root, color, color.getChildAt(0)); + test(root, 4, createFinder(color.getChildAt(1)), root, color, color.getChildAt(1)); + test(root, 5, createFinder(color.getChildAt(2)), root, color, color.getChildAt(2)); + test(root, 3, createFinder(digit), root, digit); + test(root, 4, createFinder(digit.getChildAt(0)), root, digit, digit.getChildAt(0)); + test(root, 5, createFinder(digit.getChildAt(1)), root, digit, digit.getChildAt(1)); + test(root, 6, createFinder(digit.getChildAt(2)), root, digit, digit.getChildAt(2)); + test(root, 7, createFinder(digit.getChildAt(3)), root, digit, digit.getChildAt(3)); + test(root, 8, createFinder(digit.getChildAt(4)), root, digit, digit.getChildAt(4)); + test(root, 9, createFinder(digit.getChildAt(5)), root, digit, digit.getChildAt(5)); + test(root, 10, createFinder(digit.getChildAt(6)), root, digit, digit.getChildAt(6)); + test(root, 11, createFinder(digit.getChildAt(7)), root, digit, digit.getChildAt(7)); + test(root, 12, createFinder(digit.getChildAt(8)), root, digit, digit.getChildAt(8)); + test(root, 4, createFinder(greek), root, greek); + test(root, 5, createFinder(greek.getChildAt(0)), root, greek, greek.getChildAt(0)); + test(root, 6, createFinder(greek.getChildAt(1)), root, greek, greek.getChildAt(1)); + test(root, 7, createFinder(greek.getChildAt(2)), root, greek, greek.getChildAt(2)); + test(root, 8, createFinder(greek.getChildAt(3)), root, greek, greek.getChildAt(3)); + test(root, 9, createFinder(greek.getChildAt(4)), root, greek, greek.getChildAt(4)); + } + + @Test + public void testColorFinder() { + Node color = createColorNode(); + Node digit = createDigitNode(); + Node greek = createGreekNode(); + Node root = new Node("root", color, digit, greek); + TreePath parent = new TreePath(root); + test(parent, color, 1, createFinder(root)); + test(parent, color, 1, createFinder(color), root, color); + test(parent, color, 2, createFinder(color.getChildAt(0)), root, color, color.getChildAt(0)); + test(parent, color, 3, createFinder(color.getChildAt(1)), root, color, color.getChildAt(1)); + test(parent, color, 4, createFinder(color.getChildAt(2)), root, color, color.getChildAt(2)); + test(parent, color, 1, createFinder(digit)); + test(parent, color, 1, createFinder(greek)); + } + + @Test + public void testDigitFinder() { + TreeNode root = createRoot(); + TreeNode color = root.getChildAt(0); + TreeNode digit = root.getChildAt(1); + TreeNode greek = root.getChildAt(2); + TreePath parent = new TreePath(root); + test(parent, digit, 1, createFinder(root)); + test(parent, digit, 1, createFinder(color)); + test(parent, digit, 1, createFinder(digit), root, digit); + test(parent, digit, 2, createFinder(digit.getChildAt(0)), root, digit, digit.getChildAt(0)); + test(parent, digit, 3, createFinder(digit.getChildAt(1)), root, digit, digit.getChildAt(1)); + test(parent, digit, 4, createFinder(digit.getChildAt(2)), root, digit, digit.getChildAt(2)); + test(parent, digit, 5, createFinder(digit.getChildAt(3)), root, digit, digit.getChildAt(3)); + test(parent, digit, 6, createFinder(digit.getChildAt(4)), root, digit, digit.getChildAt(4)); + test(parent, digit, 7, createFinder(digit.getChildAt(5)), root, digit, digit.getChildAt(5)); + test(parent, digit, 8, createFinder(digit.getChildAt(6)), root, digit, digit.getChildAt(6)); + test(parent, digit, 9, createFinder(digit.getChildAt(7)), root, digit, digit.getChildAt(7)); + test(parent, digit, 10, createFinder(digit.getChildAt(8)), root, digit, digit.getChildAt(8)); + test(parent, digit, 1, createFinder(greek)); + } + + @Test + public void testGreekFinder() { + TreeNode root = createRoot(); + TreeNode color = root.getChildAt(0); + TreeNode digit = root.getChildAt(1); + TreeNode greek = root.getChildAt(2); + TreePath parent = new TreePath(root); + test(parent, greek, 1, createFinder(root)); + test(parent, greek, 1, createFinder(color)); + test(parent, greek, 1, createFinder(digit)); + test(parent, greek, 1, createFinder(greek), root, greek); + test(parent, greek, 2, createFinder(greek.getChildAt(0)), root, greek, greek.getChildAt(0)); + test(parent, greek, 3, createFinder(greek.getChildAt(1)), root, greek, greek.getChildAt(1)); + test(parent, greek, 4, createFinder(greek.getChildAt(2)), root, greek, greek.getChildAt(2)); + test(parent, greek, 5, createFinder(greek.getChildAt(3)), root, greek, greek.getChildAt(3)); + test(parent, greek, 6, createFinder(greek.getChildAt(4)), root, greek, greek.getChildAt(4)); + } + + private static TreeVisitor createFinder(TreeNode node) { + return new TreeVisitor.Finder() { + @Override + protected boolean found(@NotNull TreePath path) { + return node.equals(path.getLastPathComponent()); + } + + @Override + protected boolean contains(@NotNull TreePath path) { + Object component = path.getLastPathComponent(); + return component instanceof Node && node instanceof Node && ((Node)node).isNodeAncestor((Node)component); + } + }; + } + + @Test + public void testPathFinder() { + TreeNode root = createRoot(); + TreeNode color = root.getChildAt(0); + TreeNode digit = root.getChildAt(1); + TreeNode greek = root.getChildAt(2); + test(root, 1, createPathFinder("root"), root); + test(root, 1, createPathFinder("toor")); // not found + test(root, 2, createPathFinder("root", "color"), root, color); + test(root, 4, createPathFinder("root", "roloc")); // not found + test(root, 3, createPathFinder("root", "color", "red"), root, color, color.getChildAt(0)); + test(root, 7, createPathFinder("root", "color", "der")); // not found + test(root, 4, createPathFinder("root", "color", "green"), root, color, color.getChildAt(1)); + test(root, 7, createPathFinder("root", "color", "neerg")); // not found + test(root, 5, createPathFinder("root", "color", "blue"), root, color, color.getChildAt(2)); + test(root, 7, createPathFinder("root", "color", "eulb")); // not found + test(root, 3, createPathFinder("root", "digit"), root, digit); + test(root, 4, createPathFinder("root", "tigid")); // not found + test(root, 4, createPathFinder("root", "digit", "one"), root, digit, digit.getChildAt(0)); + test(root, 13, createPathFinder("root", "digit", "eno")); // not found + test(root, 5, createPathFinder("root", "digit", "two"), root, digit, digit.getChildAt(1)); + test(root, 13, createPathFinder("root", "digit", "owt")); // not found + test(root, 6, createPathFinder("root", "digit", "three"), root, digit, digit.getChildAt(2)); + test(root, 13, createPathFinder("root", "digit", "eerht")); // not found + test(root, 7, createPathFinder("root", "digit", "four"), root, digit, digit.getChildAt(3)); + test(root, 13, createPathFinder("root", "digit", "ruof")); // not found + test(root, 8, createPathFinder("root", "digit", "five"), root, digit, digit.getChildAt(4)); + test(root, 13, createPathFinder("root", "digit", "evif")); // not found + test(root, 9, createPathFinder("root", "digit", "six"), root, digit, digit.getChildAt(5)); + test(root, 13, createPathFinder("root", "digit", "xis")); // not found + test(root, 10, createPathFinder("root", "digit", "seven"), root, digit, digit.getChildAt(6)); + test(root, 13, createPathFinder("root", "digit", "neves")); // not found + test(root, 11, createPathFinder("root", "digit", "eight"), root, digit, digit.getChildAt(7)); + test(root, 13, createPathFinder("root", "digit", "thgie")); // not found + test(root, 12, createPathFinder("root", "digit", "nine"), root, digit, digit.getChildAt(8)); + test(root, 13, createPathFinder("root", "digit", "enin")); // not found + test(root, 4, createPathFinder("root", "greek"), root, greek); + test(root, 4, createPathFinder("root", "keerg")); // not found + test(root, 5, createPathFinder("root", "greek", "alpha"), root, greek, greek.getChildAt(0)); + test(root, 9, createPathFinder("root", "greek", "ahpla")); // not found + test(root, 6, createPathFinder("root", "greek", "beta"), root, greek, greek.getChildAt(1)); + test(root, 9, createPathFinder("root", "greek", "ateb")); // not found + test(root, 7, createPathFinder("root", "greek", "gamma"), root, greek, greek.getChildAt(2)); + test(root, 9, createPathFinder("root", "greek", "ammag")); // not found + test(root, 8, createPathFinder("root", "greek", "delta"), root, greek, greek.getChildAt(3)); + test(root, 9, createPathFinder("root", "greek", "atled")); // not found + test(root, 9, createPathFinder("root", "greek", "epsilon"), root, greek, greek.getChildAt(4)); + test(root, 9, createPathFinder("root", "greek", "nolispe")); // not found + } + + @Test + public void testColorPathFinder() { + TreeNode root = createRoot(); + TreeNode color = root.getChildAt(0); + TreePath parent = new TreePath(root); + test(parent, color, 1, createPathFinder("root")); + test(parent, color, 1, createPathFinder("toor")); // not found + test(parent, color, 1, createPathFinder("root", "color"), root, color); + test(parent, color, 1, createPathFinder("root", "roloc")); // not found + test(parent, color, 2, createPathFinder("root", "color", "red"), root, color, color.getChildAt(0)); + test(parent, color, 4, createPathFinder("root", "color", "der")); // not found + test(parent, color, 3, createPathFinder("root", "color", "green"), root, color, color.getChildAt(1)); + test(parent, color, 4, createPathFinder("root", "color", "neerg")); // not found + test(parent, color, 4, createPathFinder("root", "color", "blue"), root, color, color.getChildAt(2)); + test(parent, color, 4, createPathFinder("root", "color", "eulb")); // not found + test(parent, color, 1, createPathFinder("root", "digit")); + test(parent, color, 1, createPathFinder("root", "tigid")); // not found + test(parent, color, 1, createPathFinder("root", "greek")); + test(parent, color, 1, createPathFinder("root", "keerg")); // not found + } + + @Test + public void testDigitPathFinder() { + TreeNode root = createRoot(); + TreeNode digit = root.getChildAt(1); + TreePath parent = new TreePath(root); + test(parent, digit, 1, createPathFinder("root")); + test(parent, digit, 1, createPathFinder("toor")); // not found + test(parent, digit, 1, createPathFinder("root", "color")); + test(parent, digit, 1, createPathFinder("root", "roloc")); // not found + test(parent, digit, 1, createPathFinder("root", "digit"), root, digit); + test(parent, digit, 1, createPathFinder("root", "tigid")); // not found + test(parent, digit, 2, createPathFinder("root", "digit", "one"), root, digit, digit.getChildAt(0)); + test(parent, digit, 10, createPathFinder("root", "digit", "eno")); // not found + test(parent, digit, 3, createPathFinder("root", "digit", "two"), root, digit, digit.getChildAt(1)); + test(parent, digit, 10, createPathFinder("root", "digit", "owt")); // not found + test(parent, digit, 4, createPathFinder("root", "digit", "three"), root, digit, digit.getChildAt(2)); + test(parent, digit, 10, createPathFinder("root", "digit", "eerht")); // not found + test(parent, digit, 5, createPathFinder("root", "digit", "four"), root, digit, digit.getChildAt(3)); + test(parent, digit, 10, createPathFinder("root", "digit", "ruof")); // not found + test(parent, digit, 6, createPathFinder("root", "digit", "five"), root, digit, digit.getChildAt(4)); + test(parent, digit, 10, createPathFinder("root", "digit", "evif")); // not found + test(parent, digit, 7, createPathFinder("root", "digit", "six"), root, digit, digit.getChildAt(5)); + test(parent, digit, 10, createPathFinder("root", "digit", "xis")); // not found + test(parent, digit, 8, createPathFinder("root", "digit", "seven"), root, digit, digit.getChildAt(6)); + test(parent, digit, 10, createPathFinder("root", "digit", "neves")); // not found + test(parent, digit, 9, createPathFinder("root", "digit", "eight"), root, digit, digit.getChildAt(7)); + test(parent, digit, 10, createPathFinder("root", "digit", "thgie")); // not found + test(parent, digit, 10, createPathFinder("root", "digit", "nine"), root, digit, digit.getChildAt(8)); + test(parent, digit, 10, createPathFinder("root", "digit", "enin")); // not found + test(parent, digit, 1, createPathFinder("root", "greek")); + test(parent, digit, 1, createPathFinder("root", "keerg")); // not found + } + + @Test + public void testGreekPathFinder() { + TreeNode root = createRoot(); + TreeNode greek = root.getChildAt(2); + TreePath parent = new TreePath(root); + test(parent, greek, 1, createPathFinder("root")); + test(parent, greek, 1, createPathFinder("toor")); // not found + test(parent, greek, 1, createPathFinder("root", "color")); + test(parent, greek, 1, createPathFinder("root", "roloc")); // not found + test(parent, greek, 1, createPathFinder("root", "digit")); + test(parent, greek, 1, createPathFinder("root", "tigid")); // not found + test(parent, greek, 1, createPathFinder("root", "greek"), root, greek); + test(parent, greek, 1, createPathFinder("root", "keerg")); // not found + test(parent, greek, 2, createPathFinder("root", "greek", "alpha"), root, greek, greek.getChildAt(0)); + test(parent, greek, 6, createPathFinder("root", "greek", "ahpla")); // not found + test(parent, greek, 3, createPathFinder("root", "greek", "beta"), root, greek, greek.getChildAt(1)); + test(parent, greek, 6, createPathFinder("root", "greek", "ateb")); // not found + test(parent, greek, 4, createPathFinder("root", "greek", "gamma"), root, greek, greek.getChildAt(2)); + test(parent, greek, 6, createPathFinder("root", "greek", "ammag")); // not found + test(parent, greek, 5, createPathFinder("root", "greek", "delta"), root, greek, greek.getChildAt(3)); + test(parent, greek, 6, createPathFinder("root", "greek", "atled")); // not found + test(parent, greek, 6, createPathFinder("root", "greek", "epsilon"), root, greek, greek.getChildAt(4)); + test(parent, greek, 6, createPathFinder("root", "greek", "nolispe")); // not found + } + + private static TreeVisitor createPathFinder(String... names) { + return new TreeVisitor.PathFinder(new TreePath(names), Object::toString); + } + + private static void test(TreeNode node, int count, @NotNull TreeVisitor visitor, Object... expected) { + test(null, node, count, visitor, expected); + } + + private static void test(TreePath parent, TreeNode node, int count, TreeVisitor visitor, Object... expected) { + test(parent, node, count, false, new Walker(visitor), expected); + test(parent, node, count, false, new Walker(visitor) { + @Override + protected Collection getChildren(@NotNull TreeNode node) { + setChildren(super.getChildren(node)); + return null; + } + }, expected); + } + + private static void test(TreePath parent, TreeNode node, int count, boolean error, Walker walker, Object... expected) { + walker.start(parent, node); + switch (walker.promise().getState()) { + case PENDING: + throw new IllegalStateException("not processed"); + case FULFILLED: + if (!error) break; + throw new IllegalStateException("not rejected"); + case REJECTED: + if (error) break; + throw new IllegalStateException("not fulfilled"); + } + TreeVisitor wrapper = getField(AbstractTreeWalker.class, walker, TreeVisitor.class, "visitor"); + assertEquals(Integer.valueOf(count), getField(Wrapper.class, wrapper, int.class, "count")); + assertResult(walker, expected); + } + + private static void assertResult(Walker walker, Object... expected) { + assertResult(walker, expected.length == 0 ? null : new TreePath(expected)); + } + + private static void assertResult(Walker walker, TreePath expected) { + assertEquals("unexpected result", expected, walker.promise().blockingGet(1)); + } + + + @NotNull + private static Node createRoot() { + return new Node("root", createColorNode(), createDigitNode(), createGreekNode()); + } + + @NotNull + private static Node createColorNode() { + return new Node("color", "red", "green", "blue"); + } + + @NotNull + private static Node createDigitNode() { + return new Node("digit", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"); + } + + @NotNull + private static Node createGreekNode() { + return new Node("greek", "alpha", "beta", "gamma", "delta", "epsilon"); + } + + + private static class Node extends DefaultMutableTreeNode { + private Node(Object content, Object... children) { + super(content); + for (Object child : children) { + add(child instanceof MutableTreeNode + ? (MutableTreeNode)child + : new Node(child)); + } + } + } + + + private static class Walker extends AbstractTreeWalker { + private Walker(TreeVisitor visitor) { + super(new Wrapper(visitor)); + } + + @Override + protected Collection getChildren(@NotNull TreeNode node) { + int count = node.getChildCount(); + ArrayList list = new ArrayList<>(count); + for (int i = 0; i < count; i++) list.add(node.getChildAt(i)); + return list; + } + } + + + private static class Wrapper implements TreeVisitor { + @SuppressWarnings("unused") + private int count; // reflection + private final TreeVisitor visitor; + + private Wrapper(TreeVisitor visitor) { + this.visitor = visitor; + if (PRINT) System.out.println("=============================="); + } + + @NotNull + @Override + public Action accept(@NotNull TreePath path) { + count++; + if (PRINT) System.out.println(path); + return visitor.accept(path); + } + } +}