001/* 002 * Copyright 2012-2016 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.cli.command; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.EnumSet; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Set; 026 027import org.springframework.boot.cli.command.status.ExitStatus; 028import org.springframework.boot.cli.util.Log; 029import org.springframework.util.Assert; 030import org.springframework.util.StringUtils; 031 032/** 033 * Main class used to run {@link Command}s. 034 * 035 * @author Phillip Webb 036 * @see #addCommand(Command) 037 * @see CommandRunner#runAndHandleErrors(String[]) 038 */ 039public class CommandRunner implements Iterable<Command> { 040 041 private static final Set<CommandException.Option> NO_EXCEPTION_OPTIONS = EnumSet 042 .noneOf(CommandException.Option.class); 043 044 private final String name; 045 046 private final List<Command> commands = new ArrayList<Command>(); 047 048 private Class<?>[] optionCommandClasses = {}; 049 050 private Class<?>[] hiddenCommandClasses = {}; 051 052 /** 053 * Create a new {@link CommandRunner} instance. 054 * @param name the name of the runner or {@code null} 055 */ 056 public CommandRunner(String name) { 057 this.name = (StringUtils.hasLength(name) ? name + " " : ""); 058 } 059 060 /** 061 * Return the name of the runner or an empty string. Non-empty names will include a 062 * trailing space character so that they can be used as a prefix. 063 * @return the name of the runner 064 */ 065 public String getName() { 066 return this.name; 067 } 068 069 /** 070 * Add the specified commands. 071 * @param commands the commands to add 072 */ 073 public void addCommands(Iterable<Command> commands) { 074 Assert.notNull(commands, "Commands must not be null"); 075 for (Command command : commands) { 076 addCommand(command); 077 } 078 } 079 080 /** 081 * Add the specified command. 082 * @param command the command to add. 083 */ 084 public void addCommand(Command command) { 085 Assert.notNull(command, "Command must not be null"); 086 this.commands.add(command); 087 } 088 089 /** 090 * Set the command classes which should be considered option commands. An option 091 * command is a special type of command that usually makes more sense to present as if 092 * it is an option. For example '--version'. 093 * @param commandClasses the classes of option commands. 094 * @see #isOptionCommand(Command) 095 */ 096 public void setOptionCommands(Class<?>... commandClasses) { 097 Assert.notNull(commandClasses, "CommandClasses must not be null"); 098 this.optionCommandClasses = commandClasses; 099 } 100 101 /** 102 * Set the command classes which should be hidden (i.e. executed but not displayed in 103 * the available commands list). 104 * @param commandClasses the classes of hidden commands 105 */ 106 public void setHiddenCommands(Class<?>... commandClasses) { 107 Assert.notNull(commandClasses, "CommandClasses must not be null"); 108 this.hiddenCommandClasses = commandClasses; 109 } 110 111 /** 112 * Returns if the specified command is an option command. 113 * @param command the command to test 114 * @return {@code true} if the command is an option command 115 * @see #setOptionCommands(Class...) 116 */ 117 public boolean isOptionCommand(Command command) { 118 return isCommandInstanceOf(command, this.optionCommandClasses); 119 } 120 121 private boolean isHiddenCommand(Command command) { 122 return isCommandInstanceOf(command, this.hiddenCommandClasses); 123 } 124 125 private boolean isCommandInstanceOf(Command command, Class<?>[] commandClasses) { 126 for (Class<?> commandClass : commandClasses) { 127 if (commandClass.isInstance(command)) { 128 return true; 129 } 130 } 131 return false; 132 } 133 134 @Override 135 public Iterator<Command> iterator() { 136 return getCommands().iterator(); 137 } 138 139 protected final List<Command> getCommands() { 140 return Collections.unmodifiableList(this.commands); 141 } 142 143 /** 144 * Find a command by name. 145 * @param name the name of the command 146 * @return the command or {@code null} if not found 147 */ 148 public Command findCommand(String name) { 149 for (Command candidate : this.commands) { 150 String candidateName = candidate.getName(); 151 if (candidateName.equals(name) || (isOptionCommand(candidate) 152 && ("--" + candidateName).equals(name))) { 153 return candidate; 154 } 155 } 156 return null; 157 } 158 159 /** 160 * Run the appropriate and handle and errors. 161 * @param args the input arguments 162 * @return a return status code (non boot is used to indicate an error) 163 */ 164 public int runAndHandleErrors(String... args) { 165 String[] argsWithoutDebugFlags = removeDebugFlags(args); 166 boolean debug = argsWithoutDebugFlags.length != args.length; 167 if (debug) { 168 System.setProperty("debug", "true"); 169 } 170 try { 171 ExitStatus result = run(argsWithoutDebugFlags); 172 // The caller will hang up if it gets a non-zero status 173 if (result != null && result.isHangup()) { 174 return (result.getCode() > 0 ? result.getCode() : 0); 175 } 176 return 0; 177 } 178 catch (NoArgumentsException ex) { 179 showUsage(); 180 return 1; 181 } 182 catch (Exception ex) { 183 return handleError(debug, ex); 184 } 185 } 186 187 private String[] removeDebugFlags(String[] args) { 188 List<String> rtn = new ArrayList<String>(args.length); 189 boolean appArgsDetected = false; 190 for (String arg : args) { 191 // Allow apps to have a -d argument 192 appArgsDetected |= "--".equals(arg); 193 if (("-d".equals(arg) || "--debug".equals(arg)) && !appArgsDetected) { 194 continue; 195 } 196 rtn.add(arg); 197 } 198 return rtn.toArray(new String[rtn.size()]); 199 } 200 201 /** 202 * Parse the arguments and run a suitable command. 203 * @param args the arguments 204 * @return the outcome of the command 205 * @throws Exception if the command fails 206 */ 207 protected ExitStatus run(String... args) throws Exception { 208 if (args.length == 0) { 209 throw new NoArgumentsException(); 210 } 211 String commandName = args[0]; 212 String[] commandArguments = Arrays.copyOfRange(args, 1, args.length); 213 Command command = findCommand(commandName); 214 if (command == null) { 215 throw new NoSuchCommandException(commandName); 216 } 217 beforeRun(command); 218 try { 219 return command.run(commandArguments); 220 } 221 finally { 222 afterRun(command); 223 } 224 } 225 226 /** 227 * Subclass hook called before a command is run. 228 * @param command the command about to run 229 */ 230 protected void beforeRun(Command command) { 231 } 232 233 /** 234 * Subclass hook called after a command has run. 235 * @param command the command that has run 236 */ 237 protected void afterRun(Command command) { 238 } 239 240 private int handleError(boolean debug, Exception ex) { 241 Set<CommandException.Option> options = NO_EXCEPTION_OPTIONS; 242 if (ex instanceof CommandException) { 243 options = ((CommandException) ex).getOptions(); 244 if (options.contains(CommandException.Option.RETHROW)) { 245 throw (CommandException) ex; 246 } 247 } 248 boolean couldNotShowMessage = false; 249 if (!options.contains(CommandException.Option.HIDE_MESSAGE)) { 250 couldNotShowMessage = !errorMessage(ex.getMessage()); 251 } 252 if (options.contains(CommandException.Option.SHOW_USAGE)) { 253 showUsage(); 254 } 255 if (debug || couldNotShowMessage 256 || options.contains(CommandException.Option.STACK_TRACE)) { 257 printStackTrace(ex); 258 } 259 return 1; 260 } 261 262 protected boolean errorMessage(String message) { 263 Log.error(message == null ? "Unexpected error" : message); 264 return message != null; 265 } 266 267 protected void showUsage() { 268 Log.infoPrint("usage: " + this.name); 269 for (Command command : this.commands) { 270 if (isOptionCommand(command)) { 271 Log.infoPrint("[--" + command.getName() + "] "); 272 } 273 } 274 Log.info(""); 275 Log.info(" <command> [<args>]"); 276 Log.info(""); 277 Log.info("Available commands are:"); 278 for (Command command : this.commands) { 279 if (!isOptionCommand(command) && !isHiddenCommand(command)) { 280 String usageHelp = command.getUsageHelp(); 281 String description = command.getDescription(); 282 Log.info(String.format("%n %1$s %2$-15s%n %3$s", command.getName(), 283 (usageHelp == null ? "" : usageHelp), 284 (description == null ? "" : description))); 285 } 286 } 287 Log.info(""); 288 Log.info("Common options:"); 289 Log.info(String.format("%n %1$s %2$-15s%n %3$s", "-d, --debug", 290 "Verbose mode", 291 "Print additional status information for the command you are running")); 292 Log.info(""); 293 Log.info(""); 294 Log.info("See '" + this.name 295 + "help <command>' for more information on a specific command."); 296 } 297 298 protected void printStackTrace(Exception ex) { 299 Log.error(""); 300 Log.error(ex); 301 Log.error(""); 302 } 303 304}