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}