changeset 1817:1695efcce99d

Add TreeCompleter to complete logLevels Reviewed-by: aazores, omajid, jkang, jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-September/016483.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-May/013657.html PR2646
author Lukasz Dracz <ldracz@redhat.com>
date Tue, 23 Jun 2015 10:46:29 -0400
parents e255ee1b2f90
children 912b35b5bedd
files common/core/src/main/java/com/redhat/thermostat/common/utils/LoggingUtils.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/ShellCommand.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/TabCompletion.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/TreeCompleter.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/ShellCommandTest.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/TreeCompleterTest.java
diffstat 6 files changed, 934 insertions(+), 46 deletions(-) [+]
line wrap: on
line diff
--- a/common/core/src/main/java/com/redhat/thermostat/common/utils/LoggingUtils.java	Tue Sep 29 10:20:19 2015 +0200
+++ b/common/core/src/main/java/com/redhat/thermostat/common/utils/LoggingUtils.java	Tue Jun 23 10:46:29 2015 -0400
@@ -62,14 +62,42 @@
     
     private static final String JUL_CONFIG_PROP_FILE = "java.util.logging.config.file";
 
+    public enum LogLevel {
+        /*
+         * Custom log level, intended for use with Thermostat's internal performance
+         * analysis framework.  Log messages at this level should be formatted using
+         * {@link com.redhat.thermostat.shared.perflog.PerformanceLogFormatter}.
+         */
+        PERFLOG(new Level("PERFLOG", 50) {
+            private static final long serialVersionUID = 1L;
+        }),
+        ALL(Level.ALL),
+        CONFIG(Level.CONFIG),
+        FINE(Level.FINE),
+        FINER(Level.FINER),
+        FINEST(Level.FINEST),
+        INFO(Level.INFO),
+        OFF(Level.OFF),
+        SEVERE(Level.SEVERE),
+        WARNING(Level.WARNING);
+
+        private Level level;
+
+        LogLevel(Level level) {
+            this.level = level;
+        }
+
+        public Level getLevel() {
+            return level;
+        }
+    }
+    
     /*
      * Custom log level, intended for use with Thermostat's internal performance
      * analysis framework.  Log messages at this level should be formatted using
      * {@link com.redhat.thermostat.shared.perflog.PerformanceLogFormatter}.
      */
-    public static final Level PERFLOG = new Level("PERFLOG", 50) {
-        private static final long serialVersionUID = 1L;
-    };
+    public static final Level PERFLOG = LogLevel.PERFLOG.getLevel();
 
     // package private for testing
     static final String ROOTNAME = "com.redhat.thermostat";
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/ShellCommand.java	Tue Sep 29 10:20:19 2015 +0200
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/ShellCommand.java	Tue Jun 23 10:46:29 2015 -0400
@@ -37,17 +37,14 @@
 package com.redhat.thermostat.launcher.internal;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Map;
-import java.util.Collection;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import jline.Terminal;
 import jline.TerminalFactory;
 import jline.console.ConsoleReader;
-import jline.console.completer.FileNameCompleter;
 import jline.console.history.FileHistory;
 import jline.console.history.History;
 import jline.console.history.PersistentHistory;
@@ -68,11 +65,6 @@
 import com.redhat.thermostat.shared.config.InvalidConfigurationException;
 import com.redhat.thermostat.shared.locale.Translate;
 import com.redhat.thermostat.storage.core.DbService;
-import jline.console.completer.AggregateCompleter;
-import jline.console.completer.ArgumentCompleter;
-import jline.console.completer.Completer;
-import jline.console.completer.StringsCompleter;
-import org.apache.commons.cli.Option;
 
 public class ShellCommand extends AbstractCommand {
 
@@ -163,7 +155,7 @@
     private void shellMainLoop(CommandContext ctx, History history, Terminal term) throws IOException, CommandException {
         ConsoleReader reader = new ConsoleReader(ctx.getConsole().getInput(), ctx.getConsole().getOutput(), term);
         if (reader.getCompleters().isEmpty() && commandInfoSource != null) {
-            setupTabCompletion(reader);
+            TabCompletion.setupTabCompletion(reader, commandInfoSource);
         }
         if (history != null) {
             reader.setHistory(history);
@@ -242,38 +234,5 @@
         this.commandInfoSource = source;
     }
 
-    private void setupTabCompletion(ConsoleReader reader) {
-        Collection<Completer> commands = new ArrayList<>();
-        Collection<String> options = new ArrayList<>();
-        Collection<Completer> completers = new ArrayList<>();
-        for (CommandInfo info : commandInfoSource.getCommandInfos()) {
-
-            if (info.getEnvironments().contains(Environment.SHELL)) {
-                commands.clear();
-                options.clear();
-                commands.add(new StringsCompleter(info.getName()));
-                for (Option option : (Collection<Option>) info.getOptions().getOptions()) {
-                    if (option.getLongOpt() != null) {
-                        options.add("--" + option.getLongOpt());
-                    }
-                    if (option.getOpt() != null) {
-                        options.add("-" + option.getOpt());
-                    }
-                }
-
-                if (info.needsFileTabCompletions()) {
-                    AggregateCompleter optionsAndFiles = new AggregateCompleter(new StringsCompleter(options), new FileNameCompleter());
-                    commands.add(optionsAndFiles);
-                } else {
-                    commands.add(new StringsCompleter(options));
-                }
-
-                completers.add(new ArgumentCompleter(new ArrayList<>(commands)));
-            }
-        }
-        AggregateCompleter aggregateCompleter = new AggregateCompleter(completers);
-        reader.addCompleter(aggregateCompleter);
-    }
-
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/TabCompletion.java	Tue Jun 23 10:46:29 2015 -0400
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.launcher.internal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import com.redhat.thermostat.common.utils.LoggingUtils;
+import jline.console.ConsoleReader;
+import jline.console.completer.FileNameCompleter;
+import org.apache.commons.cli.Option;
+
+import static com.redhat.thermostat.launcher.internal.TreeCompleter.createStringNode;
+
+public class TabCompletion {
+
+    private static final String LONG_OPTION_PREFIX = "--";
+    private static final String SHORT_OPTION_PREFIX = "-";
+
+    public static void setupTabCompletion(ConsoleReader reader, CommandInfoSource commandInfoSource) {
+        List<String> logLevels = new ArrayList<>();
+
+        for (LoggingUtils.LogLevel level : LoggingUtils.LogLevel.values()) {
+            logLevels.add(level.getLevel().getName());
+        }
+
+        TreeCompleter treeCompleter = new TreeCompleter();
+        for (CommandInfo info : commandInfoSource.getCommandInfos()) {
+
+            if (info.getEnvironments().contains(Environment.SHELL)) {
+                String commandName = info.getName();
+                TreeCompleter.Node command = createStringNode(commandName);
+
+                for (Option option : (Collection<Option>) info.getOptions().getOptions()) {
+                    if (option.getLongOpt().equals("logLevel")) {
+                        setupLogLevelCompletion(logLevels, command, option);
+                    } else {
+                        setupDefaultCompletion(command, option);
+                    }
+
+                }
+
+                if (info.needsFileTabCompletions()) {
+                    TreeCompleter.Node files = new TreeCompleter.Node(new FileNameCompleter());
+                    files.setRestartNode(command);
+                    command.addBranch(files);
+                }
+                treeCompleter.addBranch(command);
+            }
+        }
+
+        reader.addCompleter(treeCompleter);
+    }
+
+    private static void setupDefaultCompletion(final TreeCompleter.Node command, final Option option) {
+        if (option.getLongOpt() != null) {
+            String optionLongName = LONG_OPTION_PREFIX + option.getLongOpt();
+            TreeCompleter.Node defaultNode = createStringNode(optionLongName);
+            defaultNode.setRestartNode(command);
+            command.addBranch(defaultNode);
+        }
+        if (option.getOpt() != null) {
+            String optionShortName = SHORT_OPTION_PREFIX + option.getOpt();
+            TreeCompleter.Node defaultNode = createStringNode(optionShortName);
+            defaultNode.setRestartNode(command);
+            command.addBranch(defaultNode);
+        }
+    }
+
+    private static void setupLogLevelCompletion( final List<String> logLevels, final TreeCompleter.Node command, final Option option) {
+        if (option.getLongOpt() != null) {
+            String optionLongName = LONG_OPTION_PREFIX + option.getLongOpt();
+            TreeCompleter.Node logLevelOption = createStringNode(optionLongName);
+            TreeCompleter.Node logLevelChoices = createStringNode(logLevels);
+            logLevelChoices.setRestartNode(command);
+            logLevelOption.addBranch(logLevelChoices);
+            command.addBranch(logLevelOption);
+        }
+        if (option.getOpt() != null) {
+            String optionShortName = SHORT_OPTION_PREFIX + option.getOpt();
+            TreeCompleter.Node logLevelOption = createStringNode(optionShortName);
+            TreeCompleter.Node logLevelChoices = createStringNode(logLevels);
+            logLevelChoices.setRestartNode(command);
+            logLevelOption.addBranch(logLevelChoices);
+            command.addBranch(logLevelOption);
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/TreeCompleter.java	Tue Jun 23 10:46:29 2015 -0400
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.launcher.internal;
+
+import static java.util.Objects.requireNonNull;
+import static jline.console.completer.ArgumentCompleter.ArgumentDelimiter;
+import static jline.console.completer.ArgumentCompleter.ArgumentList;
+import static jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import jline.console.completer.Completer;
+import jline.console.completer.StringsCompleter;
+
+public class TreeCompleter implements Completer {
+
+    private final ArgumentDelimiter delimiter;
+
+    private final List<Node> branches;
+
+    private Node currentNode;
+    private ArgumentList list;
+
+    public static final int NOT_FOUND = -1;
+    private static final String EMPTY_SPACE = " ";
+    private static final Node START_NODE = null;
+
+    /**
+     * This method adds a child branch to the start node
+     * @param child a node containing a completer
+     */
+    public void addBranch(final Node child) {
+        branches.add(child);
+    }
+
+    /**
+     * This method adds child branches to the start node
+     * @param branchList a list of nodes containing a completer each
+     */
+    public void addBranches(final List<Node> branchList) {
+        for (final Node branch : branchList) {
+            addBranch(branch);
+        }
+    }
+
+    /**
+     * @return the start node branches
+     */
+    public List<Node> getBranches() {
+        return branches;
+    }
+
+    public TreeCompleter() {
+        delimiter = new WhitespaceArgumentDelimiter();
+        branches = new ArrayList<>();
+    }
+
+    /**
+     * This method is called when attempting to tab complete
+     * @param buffer the input that will be tab completed
+     * @param cursorPosition the position of the cursorPosition within the buffer
+     * @param candidates the list of possible completions will get filled when found
+     * @return the new position of the cursorPosition, a return of NOT_FOUND means no completion
+     * was found or completion is finished, resulting in no change of the cursorPosition position
+     */
+    @Override
+    public int complete(final String buffer, final int cursorPosition, final List<CharSequence> candidates) {
+        requireNonNull(candidates);
+
+        if (cursorPosition > buffer.length()) {
+            return NOT_FOUND;
+        }
+        final String currentBuffer = buffer.substring(0, cursorPosition);
+        refreshCompleter();
+
+        list = delimiter.delimit(currentBuffer, cursorPosition);
+        if (list.getCursorArgumentIndex() < 0) {
+            return NOT_FOUND;
+        }
+
+        int position = cursorPosition;
+        currentNode = traverseBranches(currentBuffer, Arrays.asList(list.getArguments()));
+        final List<Completer> completers = getAllCompleters(currentNode);
+
+        //Complete possible arguments off a space or the current word up to the cursor
+        if (currentBuffer.endsWith(EMPTY_SPACE)) {
+            completeList(candidates, completers);
+        } else {
+            if (currentNode != START_NODE && currentNode.getBranches().isEmpty()) {
+                currentNode = currentNode.getRestartNode();
+            }
+            final List<Completer> relevantCompleters = filterRelevantCompleters(completers, list.getCursorArgument());
+            for (final Completer completer : relevantCompleters) {
+                position = getInlinedCursorPosition(completer, candidates);
+            }
+        }
+
+        return position;
+    }
+
+    private int getInlinedCursorPosition(final Completer completer, final List<CharSequence> candidates) {
+        int cursor = completer.complete(list.getCursorArgument(), list.getArgumentPosition(), candidates);
+        return cursor + list.getBufferPosition() - list.getArgumentPosition();
+    }
+
+    private void refreshCompleter() {
+        currentNode = START_NODE;
+    }
+
+    private Node traverseBranches(final String currentBuffer, final List<String> arguments) {
+        Node resultNode = START_NODE;
+        for (final Iterator<String> it = arguments.iterator(); it.hasNext();) {
+            final String arg = it.next();
+            if (!it.hasNext() && !currentBuffer.endsWith(" ")) {
+                //inline completion detected
+                break;
+            }
+            final int branchIndex = getBranchIndex(arg, getPossibleMatches(resultNode));
+            if (branchIndex != NOT_FOUND) {
+                resultNode = findBranches(resultNode).get(branchIndex);
+            }
+        }
+        return resultNode;
+    }
+
+    private void completeList(final List<CharSequence> candidates, final List<Completer> completerList) {
+        for (final Completer completer : completerList) {
+            completer.complete(null, 0, candidates);
+        }
+    }
+
+    private List<Completer> getAllCompleters(final Node currentNode) {
+        final List<Completer> completersFromBranches = new ArrayList<>();
+        for (final Node node : findBranches(currentNode)) {
+            completersFromBranches.add(node.getCompleter());
+        }
+        return completersFromBranches;
+    }
+
+    private List<Node> getChildNodesFromRestartNode(final Node node) {
+        final List<Node> childrenNodeList;
+        if (node.getRestartNode() == START_NODE) {
+            childrenNodeList = getBranches();
+        } else {
+            childrenNodeList = node.getRestartNode().getBranches();
+        }
+        currentNode = node.getRestartNode();
+        return childrenNodeList;
+    }
+
+    private List<Completer> filterRelevantCompleters(final List<Completer> completersFromBranches, final String cursorArgument) {
+        final List<Completer> completers = new ArrayList<>();
+        for (final Completer branchCompleter : completersFromBranches) {
+            final List<CharSequence> candidates = new LinkedList<>();
+            branchCompleter.complete(cursorArgument, 0, candidates);
+            if (!candidates.isEmpty()) {
+                completers.add(branchCompleter);
+            }
+        }
+        return completers;
+    }
+
+    private List<CharSequence> findCompletions(final Completer branchCompleter) {
+        final List<CharSequence> candidates = new LinkedList<>();
+        branchCompleter.complete(null, 0, candidates);
+        return candidates;
+    }
+
+    private int getBranchIndex(final String argument, final List<List<CharSequence>> listOfPossibleMatches) {
+        for (final List<CharSequence> possibleMatches : listOfPossibleMatches) {
+            for (final CharSequence word : possibleMatches) {
+                if (word.toString().trim().equals(argument.trim())) {
+                    return listOfPossibleMatches.indexOf(possibleMatches);
+                }
+            }
+        }
+        return NOT_FOUND;
+    }
+
+    private List<List<CharSequence>> getPossibleMatches(final Node node) {
+        final List<List<CharSequence>> possibleMatches = new LinkedList<>();
+        for (final Node branch : findBranches(node)) {
+            possibleMatches.add(findCompletions(branch.getCompleter()));
+        }
+        return possibleMatches;
+    }
+
+    private List<Node> findBranches(final Node node) {
+        if (node == START_NODE) {
+            return getBranches();
+        } else {
+            if (node.getBranches().isEmpty()) {
+                return getChildNodesFromRestartNode(node);
+            }
+            return node.getBranches();
+        }
+    }
+
+    /**
+     * A class to be used with the TreeCompleter
+     * Each node contains a completer that will be used by the TreeCompleter
+     * to check completion. Each node contains branches that are possible
+     * paths for the completion to follow. Once completion has reached a node
+     * with no branches it will use the restartNode to continue to find
+     * any further completions.
+     */
+    public static class Node {
+        private final Completer completer;
+        private final List<Node> branches;
+        private Node restartNode = START_NODE;
+
+        public Node(final Completer data) {
+            requireNonNull(data);
+            this.completer = data;
+            branches = new ArrayList<>();
+        }
+
+        public void addBranch(final Node branch) {
+            branches.add(branch);
+        }
+
+        public Completer getCompleter() {
+            return completer;
+        }
+
+        public List<Node> getBranches() {
+            return branches;
+        }
+
+        public void setRestartNode(final Node restartNode) {
+            this.restartNode = restartNode;
+        }
+
+        public Node getRestartNode() {
+            return restartNode;
+        }
+
+    }
+
+    /**
+     * A helper method to quickly create a node containing a strings completer
+     * @param strings the strings to be completed by the strings completer
+     * @return the node containing the string completer
+     */
+    public static Node createStringNode(String... strings) {
+        return new Node(new StringsCompleter(strings));
+    }
+
+    /**
+     * A helper method to quickly create a node containing a strings completer
+     * @param strings the strings to be completed by the strings completer
+     * @return the node containing the string completer
+     */
+    public static Node createStringNode(List<String> strings) {
+        return new Node(new StringsCompleter(strings));
+    }
+}
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ShellCommandTest.java	Tue Sep 29 10:20:19 2015 +0200
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/ShellCommandTest.java	Tue Jun 23 10:46:29 2015 -0400
@@ -577,7 +577,7 @@
         createTempFile(filename + "12345678");
 
         TestCommandContextFactory ctxFactory = new TestCommandContextFactory(bundleContext);
-        ctxFactory.setInput("validate --dbUrl -a -l -d " + dir.getAbsolutePath() + File.separator + "testFil\t\nexit\n");
+        ctxFactory.setInput("validate --dbUrl -d " + dir.getAbsolutePath() + File.separator + "testFil\t\nexit\n");
         Arguments args = new SimpleArguments();
         CommandContext ctx = ctxFactory.createContext(args);
         cmd.run(ctx);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/TreeCompleterTest.java	Tue Jun 23 10:46:29 2015 -0400
@@ -0,0 +1,482 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.launcher.internal;
+
+import static com.redhat.thermostat.launcher.internal.TreeCompleter.createStringNode;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.redhat.thermostat.common.cli.CommandException;
+import jline.console.completer.FileNameCompleter;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TreeCompleterTest {
+
+    private File testDir;
+    private TreeCompleter tree;
+
+    @Before
+    public void setUp() throws IOException {
+        tree = new TreeCompleter();
+
+        testDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "treecompleter");
+        testDir.deleteOnExit();
+        testDir.mkdirs();
+        setupTreeCompleter();
+    }
+
+    @Test
+    public void testInlineCompletion() throws IOException, CommandException {
+        List<String> candidates = completeBuffer("other");
+
+        assertTrue(candidates.contains("otherCommand"));
+    }
+
+    @Test
+    public void testBaseIndexCompletion() throws IOException, CommandException {
+        List<String> candidates = completeBuffer("");
+
+        assertTrue(candidates.contains("otherCommand"));
+        assertTrue(candidates.contains("command1"));
+        assertTrue(candidates.contains("command2"));
+        assertTrue(candidates.contains("anotherCommand"));
+    }
+
+    @Test
+    public void testVeryLongCompletion() throws IOException, CommandException {
+        String[] longChainOfWords = { "otherCommand", "create", "a", "long", "chain", "of", "tab", "completing", "words"};
+        List<String> chain = new ArrayList<String>();
+        Collections.addAll(chain, longChainOfWords);
+        String firstPart = "";
+        for (String word : chain) {
+            firstPart += word + " ";
+            List<String> tabOutput = completeBuffer(firstPart + " ");
+
+            if (chain.indexOf(word) == 0) {
+                assertTrue(tabOutput.contains("list"));
+                assertTrue(tabOutput.contains("create"));
+                assertTrue(tabOutput.contains("start"));
+            } else if (chain.indexOf(word) == chain.size() - 1) {
+                assertTrue(tabOutput.contains("otherCommand"));
+                assertTrue(tabOutput.contains("anotherCommand"));
+                assertTrue(tabOutput.contains("command1"));
+                assertTrue(tabOutput.contains("command2"));
+            } else {
+                assertTrue(tabOutput.contains(chain.get(chain.indexOf(word) + 1)));
+            }
+
+        }
+    }
+
+    @Test
+    public void testVeryLongCompletionRestartedTwice() throws IOException, CommandException {
+        String[] longChainOfWords = { "otherCommand", "create", "a", "long", "chain", "of", "tab", "completing", "words"};
+        List<String> chain = new ArrayList<String>();
+        Collections.addAll(chain, longChainOfWords);
+        Collections.addAll(chain, longChainOfWords);
+        Collections.addAll(chain, longChainOfWords);
+        String firstPart = "";
+        for (String word : chain) {
+            firstPart += word + " ";
+            List<String> tabOutput = completeBuffer(firstPart + " ");
+
+            if (chain.indexOf(word) == 0) {
+                assertTrue(tabOutput.contains("list"));
+                assertTrue(tabOutput.contains("create"));
+                assertTrue(tabOutput.contains("start"));
+            } else if (chain.indexOf(word) == chain.size() - 1) {
+                assertTrue(tabOutput.contains("otherCommand"));
+                assertTrue(tabOutput.contains("anotherCommand"));
+                assertTrue(tabOutput.contains("command1"));
+                assertTrue(tabOutput.contains("command2"));
+            } else {
+                assertTrue(tabOutput.contains(chain.get(chain.indexOf(word) + 1)));
+            }
+
+        }
+    }
+
+    @Test
+    public void testRestartIndexOtherThanZero() throws CommandException {
+        String[] input = { "command1", "list", "nothing"};
+        List<String> chain = new ArrayList<String>();
+        Collections.addAll(chain, input);
+        String firstPart = "";
+        for (String word : chain) {
+            firstPart += word + " ";
+            List<String> tabOutput = completeBuffer(firstPart + " ");
+
+            if (chain.indexOf(word) == 0) {
+                assertTrue(tabOutput.contains("list"));
+                assertTrue(tabOutput.contains("create"));
+                assertTrue(tabOutput.contains("delete"));
+            } else if (chain.indexOf(word) == chain.size() - 1) {
+                assertTrue(tabOutput.contains("list"));
+                assertTrue(tabOutput.contains("create"));
+                assertTrue(tabOutput.contains("delete"));
+            } else {
+                assertTrue(tabOutput.contains(chain.get(chain.indexOf(word) + 1)));
+            }
+
+        }
+    }
+
+    @Test
+    public void testCompletionOfSameTextInDifferentBranchesReturnsDifferentResults() throws CommandException {
+        List<String> firstOutput = completeBuffer("otherCommand list ");
+        List<String> secondOutput = completeBuffer("command1 list ");
+
+        assertTrue(firstOutput.contains("parts"));
+        assertTrue(firstOutput.contains("assemblies"));
+        assertTrue(firstOutput.contains("degreesOfFreedom"));
+        assertTrue(firstOutput.contains("bolts"));
+        assertTrue(firstOutput.contains("tools"));
+
+        assertFalse(firstOutput.contains("everything"));
+        assertFalse(firstOutput.contains("nothing"));
+        assertFalse(firstOutput.contains("firstHalf"));
+        assertFalse(firstOutput.contains("secondHalf"));
+
+        assertTrue(secondOutput.contains("everything"));
+        assertTrue(secondOutput.contains("nothing"));
+        assertTrue(secondOutput.contains("firstHalf"));
+        assertTrue(secondOutput.contains("secondHalf"));
+
+        assertFalse(secondOutput.contains("parts"));
+        assertFalse(secondOutput.contains("assemblies"));
+        assertFalse(secondOutput.contains("degreesOfFreedom"));
+        assertFalse(secondOutput.contains("bolts"));
+        assertFalse(secondOutput.contains("tools"));
+    }
+
+    @Test
+    public void testRestartIndexOne() throws CommandException {
+        List<String> output = completeBuffer("command2 stop yes ");
+
+        assertTrue(output.contains("find"));
+        assertTrue(output.contains("climb"));
+        assertTrue(output.contains("stop"));
+    }
+
+    @Test
+    public void testRestartIndexTwo() throws CommandException {
+        List<String> output = completeBuffer("command1 list firstHalf ");
+
+        assertTrue(output.contains("everything"));
+        assertTrue(output.contains("nothing"));
+        assertTrue(output.contains("firstHalf"));
+        assertTrue(output.contains("secondHalf"));
+    }
+
+    @Test
+    public void testParametersDoNotInterfereWithInlineCompletion() throws CommandException {
+        List<String> inlineOutput = completeBuffer("command1 blah list blue red green notValid second");
+
+        assertTrue(inlineOutput.contains("secondHalf"));
+    }
+
+    @Test
+    public void testParametersDoNotInterfereWithCompletion() throws CommandException {
+        List<String> output = completeBuffer("otherCommand blue green list red purple ");
+
+        assertTrue(output.contains("parts"));
+        assertTrue(output.contains("assemblies"));
+        assertTrue(output.contains("degreesOfFreedom"));
+        assertTrue(output.contains("bolts"));
+        assertTrue(output.contains("tools"));
+    }
+
+    @Test
+    public void testFileCompletion() throws CommandException, IOException {
+        String filename1 = "testFilesNow";
+        String filename2 = "document";
+        String filename3 = "musicFile";
+        String filename4 = "slideshow";
+
+        createTempFile(filename1);
+        createTempFile(filename2);
+        createTempFile(filename3);
+        createTempFile(filename4);
+
+        List<String> output = completeBuffer("command2 find " + testDir.getAbsolutePath() + File.separator);
+
+        assertTrue(output.contains(filename1));
+        assertTrue(output.contains(filename2));
+        assertTrue(output.contains(filename3));
+        assertTrue(output.contains(filename4));
+    }
+
+    @Test
+    public void testFileCompletionInline() throws CommandException, IOException {
+        String filename1 = "testFilesNow";
+        String filename2 = "document";
+        String filename3 = "musicFile";
+        String filename4 = "slideshow";
+
+        createTempFile(filename1);
+        createTempFile(filename2);
+        createTempFile(filename3);
+        createTempFile(filename4);
+
+        List<String> output = completeBuffer("command2 find " + testDir.getAbsolutePath() + File.separator);
+
+        assertTrue(output.contains(filename2));
+    }
+
+    @Test
+    public void testCursorInMiddleOfWord() {
+        List<String> output = completeBuffer("command1 ", 3);
+
+        assertTrue(output.contains("command1"));
+        assertTrue(output.contains("command2"));
+        assertFalse(output.contains("otherCommand"));
+        assertFalse(output.contains("anotherCommand"));
+    }
+
+    @Test
+    public void testCursorBeforeWord() {
+        List<String> output = completeBuffer("command1 ", 0);
+
+        assertTrue(output.contains("command1"));
+        assertTrue(output.contains("command2"));
+        assertTrue(output.contains("otherCommand"));
+        assertTrue(output.contains("anotherCommand"));
+    }
+
+    @Test
+    public void testCursorBeforeSpace() {
+        List<String> output = completeBuffer("command1 ", 8);
+
+        assertTrue(output.contains("command1"));
+        assertFalse(output.contains("command2"));
+        assertFalse(output.contains("otherCommand"));
+        assertFalse(output.contains("anotherCommand"));
+    }
+
+    @Test
+    public void testCursorAtSpace() {
+        List<String> output = completeBuffer("command1 ", 9);
+
+        assertTrue(output.contains("list"));
+        assertTrue(output.contains("create"));
+        assertTrue(output.contains("delete"));
+    }
+
+    @Test
+    public void testCursorAtSpaceBeforeAnotherWord() {
+        List<String> output = completeBuffer("command1 list", 9);
+
+        assertTrue(output.contains("list"));
+        assertTrue(output.contains("create"));
+        assertTrue(output.contains("delete"));
+    }
+
+    @Test
+    public void testCursorInMiddleOfSecondWord() {
+        List<String> output = completeBuffer("command1 list", 11);
+
+        assertTrue(output.contains("list"));
+        assertFalse(output.contains("create"));
+        assertFalse(output.contains("delete"));
+    }
+
+    @Test
+    public void testCursorLongerThanBufferLength() {
+        List<CharSequence> candidates = new LinkedList<>();
+
+        int cursor = tree.complete("command1 ", 503, candidates);
+        List<String> convertedCandidates = convertToStringList(candidates);
+        assertEquals(TreeCompleter.NOT_FOUND, cursor);
+        assertTrue(convertedCandidates.isEmpty());
+    }
+
+    private List<String> completeBuffer(String buffer) {
+        return completeBuffer(buffer, buffer.length());
+    }
+
+    private List<String> completeBuffer(String buffer, int cursor) {
+        List<CharSequence> candidates = new LinkedList<>();
+
+        tree.complete(buffer, cursor, candidates);
+        List<String> convertedCandidates = convertToStringList(candidates);
+        return convertedCandidates;
+    }
+
+    private List<String> convertToStringList(List<CharSequence> list) {
+        List<String> stringsList = new ArrayList<>();
+        for (CharSequence chars : list) {
+            stringsList.add(chars.toString().trim());
+        }
+        return stringsList;
+    }
+
+    private void createTempFile(String name) throws IOException {
+        File file = new File(testDir, name);
+        file.deleteOnExit();
+        file.createNewFile();
+    }
+
+    public void setupTreeCompleter() throws IOException {
+
+        List<TreeCompleter.Node> commands = new ArrayList<>();
+        TreeCompleter.Node command1 = createStringNode("command1");
+        {
+            TreeCompleter.Node create = createStringNode("create");
+            TreeCompleter.Node delete = createStringNode("delete");
+            TreeCompleter.Node list = createStringNode("list");
+            {
+                TreeCompleter.Node everything = createStringNode("everything");
+                TreeCompleter.Node nothing = createStringNode("nothing");
+                nothing.setRestartNode(command1);
+                TreeCompleter.Node firstHalf = createStringNode("firstHalf");
+                firstHalf.setRestartNode(list);
+                TreeCompleter.Node secondHalf = createStringNode("secondHalf");
+                secondHalf.setRestartNode(list); // maybe secondHalf
+                list.addBranch(everything);
+                list.addBranch(nothing);
+                list.addBranch(firstHalf);
+                list.addBranch(secondHalf);
+            }
+            command1.addBranch(list);
+            command1.addBranch(create);
+            command1.addBranch(delete);
+        }
+
+        TreeCompleter.Node command2 = createStringNode("command2");
+        {
+            TreeCompleter.Node find = createStringNode("find");
+            {
+                TreeCompleter.Node files = new TreeCompleter.Node(new FileNameCompleter());
+                find.addBranch(files);
+            }
+            TreeCompleter.Node climb = createStringNode("climb");
+            TreeCompleter.Node stop = createStringNode("stop");
+            {
+                TreeCompleter.Node yes = createStringNode("yes");
+                yes.setRestartNode(command2);
+                TreeCompleter.Node no = createStringNode("no");
+                stop.addBranch(yes);
+                stop.addBranch(no);
+            }
+            command2.addBranch(find);
+            command2.addBranch(climb);
+            command2.addBranch(stop);
+        }
+        TreeCompleter.Node otherCommand = createStringNode("otherCommand");
+        {
+            TreeCompleter.Node list = createStringNode("list");
+            {
+                TreeCompleter.Node parts = createStringNode("parts");
+                TreeCompleter.Node assemblies = createStringNode("assemblies");
+                assemblies.setRestartNode(otherCommand);
+                TreeCompleter.Node degreesOfFreedom = createStringNode("degreesOfFreedom");
+                degreesOfFreedom.setRestartNode(list);
+                TreeCompleter.Node bolts = createStringNode("bolts");
+                bolts.setRestartNode(list);
+                TreeCompleter.Node tools = createStringNode("tools");
+                tools.setRestartNode(list);
+                list.addBranch(parts);
+                list.addBranch(assemblies);
+                list.addBranch(degreesOfFreedom);
+                list.addBranch(bolts);
+                list.addBranch(tools);
+            }
+            TreeCompleter.Node create = createStringNode("create");
+            {
+                TreeCompleter.Node a = createStringNode("a");
+                TreeCompleter.Node longNode = createStringNode("long");
+                TreeCompleter.Node chain = createStringNode("chain");
+                TreeCompleter.Node of = createStringNode("of");
+                TreeCompleter.Node tab = createStringNode("tab");
+                TreeCompleter.Node completing = createStringNode("completing");
+                TreeCompleter.Node words = createStringNode("words");
+                completing.addBranch(words);
+                tab.addBranch(completing);
+                of.addBranch(tab);
+                chain.addBranch(of);
+                longNode.addBranch(chain);
+                a.addBranch(longNode);
+                create.addBranch(a);
+            }
+            TreeCompleter.Node start = createStringNode("start");
+            {
+                TreeCompleter.Node yes = createStringNode("yes");
+                yes.setRestartNode(otherCommand);
+                TreeCompleter.Node no = createStringNode("no");
+                start.addBranch(yes);
+                start.addBranch(no);
+            }
+            otherCommand.addBranch(list);
+            otherCommand.addBranch(create);
+            otherCommand.addBranch(start);
+        }
+        TreeCompleter.Node anotherCommand = createStringNode("anotherCommand");
+        {
+            TreeCompleter.Node list = createStringNode("list");
+            {
+                TreeCompleter.Node everything = createStringNode("everything");
+                TreeCompleter.Node nothing = createStringNode("nothing");
+                TreeCompleter.Node firstHalf = createStringNode("firstHalf");
+                TreeCompleter.Node secondHalf = createStringNode("secondHalf");
+                TreeCompleter.Node twentyFifthToSeventyFifthPart = createStringNode("25to75part");
+                list.addBranch(everything);
+                list.addBranch(nothing);
+                list.addBranch(firstHalf);
+                list.addBranch(secondHalf);
+                list.addBranch(twentyFifthToSeventyFifthPart);
+            }
+            anotherCommand.addBranch(list);
+        }
+
+        commands.add(command1);
+        commands.add(command2);
+        commands.add(otherCommand);
+        commands.add(anotherCommand);
+
+        tree.addBranches(commands);
+    }
+
+}