diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java index c45c38d6d0..950613f554 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java @@ -129,6 +129,15 @@ public class SpringCli { || displayName.endsWith(" ") ? displayName : displayName + " "; } + /** + * The name of this tool when printed by the help command. + * + * @return the displayName + */ + public String getDisplayName() { + return this.displayName; + } + /** * Parse the arguments and run a suitable command. * @param args the arguments @@ -148,10 +157,11 @@ public class SpringCli { command.run(commandArguments); } - protected final Command find(String name) { + public final Command find(String name) { for (Command candidate : this.commands) { - if (candidate.getName().equals(name) - || (candidate.isOptionCommand() && ("--" + candidate.getName()) + String candidateName = candidate.getName(); + if (candidateName.equals(name) + || (candidate.isOptionCommand() && ("--" + candidateName) .equals(name))) { return candidate; } @@ -159,6 +169,25 @@ public class SpringCli { return null; } + public void register(Command command) { + Command existing = find(command.getName()); + int index = this.commands.indexOf(find("hint")) - 1; + index = index >= 0 ? index : 0; + if (existing != null) { + index = this.commands.indexOf(existing); + this.commands.remove(index); + } + this.commands.add(index, command); + } + + public void unregister(String name) { + this.commands.remove(find(name)); + } + + public List getCommands() { + return Collections.unmodifiableList(this.commands); + } + protected void showUsage() { Log.infoPrint("usage: " + this.displayName); for (Command command : this.commands) { @@ -384,4 +413,5 @@ public class SpringCli { System.exit(exitCode); } } + } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java index 26b3dfa326..ec48f4937f 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/CommandCompleter.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.ServiceLoader; import jline.console.ConsoleReader; import jline.console.completer.ArgumentCompleter; @@ -14,7 +13,6 @@ import jline.console.completer.NullCompleter; import jline.console.completer.StringsCompleter; import org.springframework.boot.cli.Command; -import org.springframework.boot.cli.CommandFactory; import org.springframework.boot.cli.Log; import org.springframework.boot.cli.OptionHelp; import org.springframework.boot.cli.SpringCli; @@ -26,62 +24,59 @@ import org.springframework.util.StringUtils; public class CommandCompleter extends StringsCompleter { private final Map optionCompleters = new HashMap(); - private List commands = new ArrayList(); + private List commands = new ArrayList(); private ConsoleReader console; - private String lastBuffer; + private String lastBuffer; public CommandCompleter(ConsoleReader console, SpringCli cli) { this.console = console; - for(CommandFactory fac : ServiceLoader.load(CommandFactory.class, getClass().getClassLoader())) { - commands.addAll(fac.getCommands(cli)); - } - + this.commands.addAll(cli.getCommands()); List names = new ArrayList(); - for(Command c : commands) { + for (Command c : this.commands) { names.add(c.getName()); List opts = new ArrayList(); - for(OptionHelp optHelp : c.getOptionsHelp()) { + for (OptionHelp optHelp : c.getOptionsHelp()) { opts.addAll(optHelp.getOptions()); } - optionCompleters.put(c.getName(), new ArgumentCompleter( - new StringsCompleter(c.getName()), - new StringsCompleter(opts), - new NullCompleter() - )); + this.optionCompleters.put(c.getName(), new ArgumentCompleter( + new StringsCompleter(c.getName()), new StringsCompleter(opts), + new NullCompleter())); } getStrings().addAll(names); + } @Override public int complete(String buffer, int cursor, List candidates) { int i = super.complete(buffer, cursor, candidates); - if(buffer.indexOf(' ') < 1) { + if (buffer.indexOf(' ') < 1) { return i; } String name = buffer.substring(0, buffer.indexOf(' ')); - if("".equals(name.trim())) { + if ("".equals(name.trim())) { return i; } - for(Command c : commands) { - if(!c.getName().equals(name)) { + for (Command c : this.commands) { + if (!c.getName().equals(name)) { continue; } - if(buffer.equals(lastBuffer)) { - lastBuffer = buffer; + if (buffer.equals(this.lastBuffer)) { + this.lastBuffer = buffer; try { - console.println(); - console.println("Usage:"); - console.println(c.getName() + " " + c.getUsageHelp()); + this.console.println(); + this.console.println("Usage:"); + this.console.println(c.getName() + " " + c.getUsageHelp()); List> rows = new ArrayList>(); int maxSize = 0; - for(OptionHelp optHelp : c.getOptionsHelp()) { + for (OptionHelp optHelp : c.getOptionsHelp()) { List cols = new ArrayList(); - for(String s : optHelp.getOptions()) { + for (String s : optHelp.getOptions()) { cols.add(s); } - String opts = StringUtils.collectionToDelimitedString(cols, " | "); - if(opts.length() > maxSize) { + String opts = StringUtils + .collectionToDelimitedString(cols, " | "); + if (opts.length() > maxSize) { maxSize = opts.length(); } cols.clear(); @@ -91,30 +86,31 @@ public class CommandCompleter extends StringsCompleter { } StringBuilder sb = new StringBuilder("\t"); - for(List row : rows) { + for (List row : rows) { String col1 = row.get(0); String col2 = row.get(1); - for(int j = 0; j < (maxSize - col1.length()); j++) { + for (int j = 0; j < (maxSize - col1.length()); j++) { sb.append(" "); } sb.append(col1).append(": ").append(col2); - console.println(sb.toString()); + this.console.println(sb.toString()); sb = new StringBuilder("\t"); } - console.drawLine(); - } catch(IOException e) { + this.console.drawLine(); + } + catch (IOException e) { Log.error(e.getMessage() + " (" + e.getClass().getName() + ")"); } } - Completer completer = optionCompleters.get(c.getName()); - if(null != completer) { + Completer completer = this.optionCompleters.get(c.getName()); + if (null != completer) { i = completer.complete(buffer, cursor, candidates); break; } } - lastBuffer = buffer; + this.lastBuffer = buffer; return i; } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java index a9c81e0927..ee4368e150 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/DefaultCommandFactory.java @@ -36,18 +36,22 @@ public class DefaultCommandFactory implements CommandFactory { new VersionCommand(), new CleanCommand(), new TestCommand(), new GrabCommand()); + private Collection commands; + @Override public Collection getCommands(SpringCli cli) { - Collection commands = new ArrayList(DEFAULT_COMMANDS); - RunCommand run = new RunCommand(); - StopCommand stop = new StopCommand(run); - ShellCommand shell = new ShellCommand(cli); - PromptCommand prompt = new PromptCommand(shell); - commands.add(run); - commands.add(stop); - commands.add(shell); - commands.add(prompt); - return commands; + if (this.commands == null) { + synchronized (this) { + if (this.commands == null) { + this.commands = new ArrayList(DEFAULT_COMMANDS); + RunCommand run = new RunCommand(); + ShellCommand shell = new ShellCommand(cli); + this.commands.add(run); + this.commands.add(shell); + } + } + } + return this.commands; } } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java index 34fb4b0f04..a661fb91e9 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java @@ -17,14 +17,17 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** + * A shell command for Spring Boot. Drops the user into an event loop (REPL) where command + * line completion and history are available without relying on OS shell features. + * * @author Jon Brisbin * @author Dave Syer */ public class ShellCommand extends AbstractCommand { - private static final String DEFAULT_PROMPT = "$ "; + private String defaultPrompt = "$ "; private SpringCli springCli; - private String prompt = DEFAULT_PROMPT; + private String prompt = this.defaultPrompt; private Stack prompts = new Stack(); public ShellCommand(SpringCli springCli) { @@ -35,7 +38,7 @@ public class ShellCommand extends AbstractCommand { @Override public void run(String... args) throws Exception { - this.springCli.setDisplayName(""); + enhance(this.springCli); final ConsoleReader console = new ConsoleReader(); console.addCompleter(new CommandCompleter(console, this.springCli)); @@ -165,11 +168,26 @@ public class ShellCommand extends AbstractCommand { } } - private void printBanner() { + protected void enhance(SpringCli cli) { + String name = cli.getDisplayName().trim(); + this.defaultPrompt = name + "> "; + this.prompt = this.defaultPrompt; + cli.setDisplayName(""); + RunCommand run = (RunCommand) cli.find("run"); + if (run != null) { + StopCommand stop = new StopCommand(run); + cli.register(stop); + } + PromptCommand prompt = new PromptCommand(this); + cli.register(prompt); + } + + protected void printBanner() { String version = ShellCommand.class.getPackage().getImplementationVersion(); version = (version == null ? "" : " (v" + version + ")"); System.out.println("Spring Boot CLI" + version); - System.out.println("Hit TAB to complete. Type 'help' and hit RETURN for help."); + System.out + .println("Hit TAB to complete. Type 'help' and hit RETURN for help, and 'quit' to exit."); } public void pushPrompt(String prompt) { @@ -179,7 +197,7 @@ public class ShellCommand extends AbstractCommand { public String popPrompt() { if (this.prompts.isEmpty()) { - this.prompt = DEFAULT_PROMPT; + this.prompt = this.defaultPrompt; } else { this.prompt = this.prompts.pop(); diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SpringCliTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SpringCliTests.java index 9d8107748c..56e0df0da7 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SpringCliTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SpringCliTests.java @@ -26,10 +26,10 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.boot.cli.SpringCli.NoArgumentsException; import org.springframework.boot.cli.SpringCli.NoHelpCommandArgumentsException; import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; @@ -51,6 +51,12 @@ public class SpringCliTests { @Mock private Command regularCommand; + @Mock + private Command shellCommand; + + @Mock + private Command anotherCommand; + private Set calls = EnumSet.noneOf(Call.class); @Before @@ -76,15 +82,17 @@ public class SpringCliTests { super.printStackTrace(ex); } }; + given(this.shellCommand.getName()).willReturn("shell"); + given(this.anotherCommand.getName()).willReturn("another"); given(this.regularCommand.getName()).willReturn("command"); given(this.regularCommand.getDescription()).willReturn("A regular command"); - this.cli.setCommands(Arrays.asList(this.regularCommand)); + this.cli.setCommands(Arrays.asList(this.regularCommand, this.shellCommand)); } @Test public void runWithoutArguments() throws Exception { - this.thrown.expect(NoArgumentsException.class); this.cli.run(); + verify(this.shellCommand).run(); } @Test @@ -93,6 +101,27 @@ public class SpringCliTests { verify(this.regularCommand).run("--arg1", "arg2"); } + @Test + public void registerCommand() throws Exception { + int before = this.cli.getCommands().size(); + this.cli.register(this.anotherCommand); + assertEquals(before + 1, this.cli.getCommands().size()); + // Just before the hint command + assertEquals(before - 2, this.cli.getCommands().indexOf(this.cli.find("another"))); + this.cli.unregister(this.anotherCommand.getName()); + assertEquals(before, this.cli.getCommands().size()); + } + + @Test + public void reRegisterCommand() throws Exception { + int index = this.cli.getCommands().indexOf(this.cli.find("regularCommand")); + int before = this.cli.getCommands().size(); + this.cli.register(this.regularCommand); + assertEquals(before, this.cli.getCommands().size()); + assertEquals(index, + this.cli.getCommands().indexOf(this.cli.find("regularCommand"))); + } + @Test public void missingCommand() throws Exception { this.thrown.expect(NoSuchCommandException.class); @@ -106,13 +135,6 @@ public class SpringCliTests { assertThat(this.calls, equalTo((Set) EnumSet.noneOf(Call.class))); } - @Test - public void handlesNoArgumentsException() throws Exception { - int status = this.cli.runAndHandleErrors(); - assertThat(status, equalTo(1)); - assertThat(this.calls, equalTo((Set) EnumSet.of(Call.SHOW_USAGE))); - } - @Test public void handlesNoSuchCommand() throws Exception { int status = this.cli.runAndHandleErrors("missing");