/*
* Copyright 2012-2017 the original author or authors.
*
* 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 org.springframework.boot.cli.command;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.util.Log;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Main class used to run {@link Command}s.
*
* @author Phillip Webb
* @see #addCommand(Command)
* @see CommandRunner#runAndHandleErrors(String[])
*/
public class CommandRunner implements Iterable<Command> {
private static final Set<CommandException.Option> NO_EXCEPTION_OPTIONS = EnumSet
.noneOf(CommandException.Option.class);
private final String name;
private final List<Command> commands = new ArrayList<>();
private Class<?>[] optionCommandClasses = {};
private Class<?>[] hiddenCommandClasses = {};
/**
* Create a new {@link CommandRunner} instance.
* @param name the name of the runner or {@code null}
*/
public CommandRunner(String name) {
this.name = (StringUtils.hasLength(name) ? name + " " : "");
}
/**
* Return the name of the runner or an empty string. Non-empty names will include a
* trailing space character so that they can be used as a prefix.
* @return the name of the runner
*/
public String getName() {
return this.name;
}
/**
* Add the specified commands.
* @param commands the commands to add
*/
public void addCommands(Iterable<Command> commands) {
Assert.notNull(commands, "Commands must not be null");
for (Command command : commands) {
addCommand(command);
}
}
/**
* Add the specified command.
* @param command the command to add.
*/
public void addCommand(Command command) {
Assert.notNull(command, "Command must not be null");
this.commands.add(command);
}
/**
* Set the command classes which should be considered option commands. An option
* command is a special type of command that usually makes more sense to present as if
* it is an option. For example '--version'.
* @param commandClasses the classes of option commands.
* @see #isOptionCommand(Command)
*/
public void setOptionCommands(Class<?>... commandClasses) {
Assert.notNull(commandClasses, "CommandClasses must not be null");
this.optionCommandClasses = commandClasses;
}
/**
* Set the command classes which should be hidden (i.e. executed but not displayed in
* the available commands list).
* @param commandClasses the classes of hidden commands
*/
public void setHiddenCommands(Class<?>... commandClasses) {
Assert.notNull(commandClasses, "CommandClasses must not be null");
this.hiddenCommandClasses = commandClasses;
}
/**
* Returns if the specified command is an option command.
* @param command the command to test
* @return {@code true} if the command is an option command
* @see #setOptionCommands(Class...)
*/
public boolean isOptionCommand(Command command) {
return isCommandInstanceOf(command, this.optionCommandClasses);
}
private boolean isHiddenCommand(Command command) {
return isCommandInstanceOf(command, this.hiddenCommandClasses);
}
private boolean isCommandInstanceOf(Command command, Class<?>[] commandClasses) {
for (Class<?> commandClass : commandClasses) {
if (commandClass.isInstance(command)) {
return true;
}
}
return false;
}
@Override
public Iterator<Command> iterator() {
return getCommands().iterator();
}
protected final List<Command> getCommands() {
return Collections.unmodifiableList(this.commands);
}
/**
* Find a command by name.
* @param name the name of the command
* @return the command or {@code null} if not found
*/
public Command findCommand(String name) {
for (Command candidate : this.commands) {
String candidateName = candidate.getName();
if (candidateName.equals(name) || (isOptionCommand(candidate)
&& ("--" + candidateName).equals(name))) {
return candidate;
}
}
return null;
}
/**
* Run the appropriate and handle and errors.
* @param args the input arguments
* @return a return status code (non boot is used to indicate an error)
*/
public int runAndHandleErrors(String... args) {
String[] argsWithoutDebugFlags = removeDebugFlags(args);
boolean debug = argsWithoutDebugFlags.length != args.length;
if (debug) {
System.setProperty("debug", "true");
}
try {
ExitStatus result = run(argsWithoutDebugFlags);
// The caller will hang up if it gets a non-zero status
if (result != null && result.isHangup()) {
return (result.getCode() > 0 ? result.getCode() : 0);
}
return 0;
}
catch (NoArgumentsException ex) {
showUsage();
return 1;
}
catch (Exception ex) {
return handleError(debug, ex);
}
}
private String[] removeDebugFlags(String[] args) {
List<String> rtn = new ArrayList<>(args.length);
boolean appArgsDetected = false;
for (String arg : args) {
// Allow apps to have a -d argument
appArgsDetected |= "--".equals(arg);
if (("-d".equals(arg) || "--debug".equals(arg)) && !appArgsDetected) {
continue;
}
rtn.add(arg);
}
return rtn.toArray(new String[rtn.size()]);
}
/**
* Parse the arguments and run a suitable command.
* @param args the arguments
* @return the outcome of the command
* @throws Exception if the command fails
*/
protected ExitStatus run(String... args) throws Exception {
if (args.length == 0) {
throw new NoArgumentsException();
}
String commandName = args[0];
String[] commandArguments = Arrays.copyOfRange(args, 1, args.length);
Command command = findCommand(commandName);
if (command == null) {
throw new NoSuchCommandException(commandName);
}
beforeRun(command);
try {
return command.run(commandArguments);
}
finally {
afterRun(command);
}
}
/**
* Subclass hook called before a command is run.
* @param command the command about to run
*/
protected void beforeRun(Command command) {
}
/**
* Subclass hook called after a command has run.
* @param command the command that has run
*/
protected void afterRun(Command command) {
}
private int handleError(boolean debug, Exception ex) {
Set<CommandException.Option> options = NO_EXCEPTION_OPTIONS;
if (ex instanceof CommandException) {
options = ((CommandException) ex).getOptions();
if (options.contains(CommandException.Option.RETHROW)) {
throw (CommandException) ex;
}
}
boolean couldNotShowMessage = false;
if (!options.contains(CommandException.Option.HIDE_MESSAGE)) {
couldNotShowMessage = !errorMessage(ex.getMessage());
}
if (options.contains(CommandException.Option.SHOW_USAGE)) {
showUsage();
}
if (debug || couldNotShowMessage
|| options.contains(CommandException.Option.STACK_TRACE)) {
printStackTrace(ex);
}
return 1;
}
protected boolean errorMessage(String message) {
Log.error(message == null ? "Unexpected error" : message);
return message != null;
}
protected void showUsage() {
Log.infoPrint("usage: " + this.name);
for (Command command : this.commands) {
if (isOptionCommand(command)) {
Log.infoPrint("[--" + command.getName() + "] ");
}
}
Log.info("");
Log.info(" <command> [<args>]");
Log.info("");
Log.info("Available commands are:");
for (Command command : this.commands) {
if (!isOptionCommand(command) && !isHiddenCommand(command)) {
String usageHelp = command.getUsageHelp();
String description = command.getDescription();
Log.info(String.format("%n %1$s %2$-15s%n %3$s", command.getName(),
(usageHelp == null ? "" : usageHelp),
(description == null ? "" : description)));
}
}
Log.info("");
Log.info("Common options:");
Log.info(String.format("%n %1$s %2$-15s%n %3$s", "-d, --debug",
"Verbose mode",
"Print additional status information for the command you are running"));
Log.info("");
Log.info("");
Log.info("See '" + this.name
+ "help <command>' for more information on a specific command.");
}
protected void printStackTrace(Exception ex) {
Log.error("");
Log.error(ex);
Log.error("");
}
}