From 5ccc7beaf4f5f90513982dcd500ed81e3db2c417 Mon Sep 17 00:00:00 2001 From: uboness Date: Sat, 2 Aug 2014 17:16:27 +0200 Subject: [PATCH] Added a cli infrastructure CliTool is a base class for command-line interface tools (such as the plugin manager and potentially others). It supports the following: - single or multi command tool - help printing infrastructure (based on help files) - consistent mechanism of parsing arguments (based on commons-cli lib) - separation of argument parsing and command execution (for easier unit testing) - terminal abstraction (will use System.console() when available) --- pom.xml | 15 + .../org/elasticsearch/common/cli/CliTool.java | 249 +++++++++++++ .../common/cli/CliToolConfig.java | 271 ++++++++++++++ .../elasticsearch/common/cli/HelpPrinter.java | 57 +++ .../elasticsearch/common/cli/Terminal.java | 176 +++++++++ .../http/client/HttpDownloadHelper.java | 23 +- .../org/elasticsearch/common/io/Streams.java | 23 ++ .../elasticsearch/common/util/Callback.java | 29 ++ .../common/cli/CliToolTestCase.java | 92 +++++ .../common/cli/CliToolTests.java | 343 ++++++++++++++++++ .../common/cli/TerminalTests.java | 89 +++++ .../elasticsearch/common/cli/tool-cmd1.help | 1 + .../org/elasticsearch/common/cli/tool.help | 1 + 13 files changed, 1362 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/elasticsearch/common/cli/CliTool.java create mode 100644 src/main/java/org/elasticsearch/common/cli/CliToolConfig.java create mode 100644 src/main/java/org/elasticsearch/common/cli/HelpPrinter.java create mode 100644 src/main/java/org/elasticsearch/common/cli/Terminal.java create mode 100644 src/main/java/org/elasticsearch/common/util/Callback.java create mode 100644 src/test/java/org/elasticsearch/common/cli/CliToolTestCase.java create mode 100644 src/test/java/org/elasticsearch/common/cli/CliToolTests.java create mode 100644 src/test/java/org/elasticsearch/common/cli/TerminalTests.java create mode 100644 src/test/resources/org/elasticsearch/common/cli/tool-cmd1.help create mode 100644 src/test/resources/org/elasticsearch/common/cli/tool.help diff --git a/pom.xml b/pom.xml index 2be7e1ac434..59564844c83 100644 --- a/pom.xml +++ b/pom.xml @@ -263,6 +263,13 @@ 3.0 compile + + + commons-cli + commons-cli + 1.2 + + @@ -667,6 +674,7 @@ com.ning:compress-lzf com.github.spullara.mustache.java:compiler com.tdunning:t-digest + commons-cli:commons-cli @@ -706,6 +714,10 @@ com.tdunning.math.stats org.elasticsearch.common.stats + + org.apache.commons.cli + org.elasticsearch.common.cli.commons + @@ -1153,7 +1165,10 @@ jsr166e/** org/elasticsearch/common/logging/log4j/ConsoleAppender* + org/elasticsearch/common/http/client/HttpDownloadHelper* + org/elasticsearch/common/cli/Terminal* org/elasticsearch/plugins/PluginManager.class + org/elasticsearch/common/http/client/HttpDownloadHelper.class org/elasticsearch/bootstrap/Bootstrap.class org/elasticsearch/Version.class org/elasticsearch/common/lucene/search/Queries$QueryWrapperFilterFactory.class diff --git a/src/main/java/org/elasticsearch/common/cli/CliTool.java b/src/main/java/org/elasticsearch/common/cli/CliTool.java new file mode 100644 index 00000000000..6b29df831eb --- /dev/null +++ b/src/main/java/org/elasticsearch/common/cli/CliTool.java @@ -0,0 +1,249 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.cli; + +import com.google.common.base.Preconditions; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.GnuParser; +import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.node.internal.InternalSettingsPreparer; + +import java.io.IOException; +import java.util.Locale; + +import static org.elasticsearch.common.settings.ImmutableSettings.Builder.EMPTY_SETTINGS; + +/** + * A base class for command-line interface tool. + * + * Two modes are supported: + * + * - Single command mode. The tool exposes a single command that can potentially accept arguments (eg. CLI options). + * - Multi command mode. The tool support multiple commands, each for different tasks, each potentially accepts arguments. + * + * In a multi-command mode. The first argument must be the command name. For example, the plugin manager + * can be seen as a multi-command tool with two possible commands: install and uninstall + * + * The tool is configured using a {@link CliToolConfig} which encapsulates the tool's commands and their + * potential options. The tool also comes with out of the box simple help support (the -h/--help option is + * automatically handled) where the help text is configured in a dedicated *.help files located in the same package + * as the tool. + */ +public abstract class CliTool { + + // based on sysexits.h + public static enum ExitStatus { + OK(0), + USAGE(64), /* command line usage error */ + DATA_ERROR(65), /* data format error */ + NO_INPUT(66), /* cannot open input */ + NO_USER(67), /* addressee unknown */ + NO_HOST(68), /* host name unknown */ + UNAVAILABLE(69), /* service unavailable */ + CODE_ERROR(70), /* internal software error */ + CANT_CREATE(73), /* can't create (user) output file */ + IO_ERROR(74), /* input/output error */ + TEMP_FAILURE(75), /* temp failure; user is invited to retry */ + PROTOCOL(76), /* remote error in protocol */ + NOPERM(77), /* permission denied */ + CONFIG(78); /* configuration error */ + + final int status; + + private ExitStatus(int status) { + this.status = status; + } + + public int status() { + return status; + } + } + + protected final Terminal terminal; + protected final Environment env; + protected final Settings settings; + + private final CliToolConfig config; + + protected CliTool(CliToolConfig config) { + this(config, Terminal.DEFAULT); + } + + protected CliTool(CliToolConfig config, Terminal terminal) { + Preconditions.checkArgument(config.cmds().size() != 0, "At least one command must be configured"); + this.config = config; + this.terminal = terminal; + Tuple tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true); + settings = tuple.v1(); + env = tuple.v2(); + } + + public final int execute(String... args) { + + // first lets see if the user requests tool help. We're doing it only if + // this is a multi-command tool. If it's a single command tool, the -h/--help + // option will be taken care of on the command level + if (!config.isSingle() && args.length > 0 && (args[0].equals("-h") || args[0].equals("--help"))) { + config.printUsage(terminal); + return ExitStatus.OK.status; + } + + CliToolConfig.Cmd cmd; + if (config.isSingle()) { + cmd = config.single(); + } else { + + if (args.length == 0) { + terminal.printError("command not specified"); + config.printUsage(terminal); + return ExitStatus.USAGE.status; + } + + String cmdName = args[0]; + cmd = config.cmd(cmdName); + if (cmd == null) { + terminal.printError("unknown command [%s]. Use [-h] option to list available commands", cmdName); + return ExitStatus.USAGE.status; + } + + // we now remove the command name from the args + if (args.length == 1) { + args = new String[0]; + } else { + String[] cmdArgs = new String[args.length - 1]; + System.arraycopy(args, 1, cmdArgs, 0, cmdArgs.length); + args = cmdArgs; + } + } + + Command command = null; + try { + + command = parse(cmd, args); + return command.execute(settings, env).status; + + } catch (IOException ioe) { + terminal.printError(ioe.getMessage()); + return ExitStatus.IO_ERROR.status; + } catch (IllegalArgumentException | ElasticsearchIllegalArgumentException ilae) { + terminal.printError(ilae.getMessage()); + return ExitStatus.USAGE.status; + } catch (Throwable t) { + terminal.printError(t.getMessage()); + if (command == null) { + return ExitStatus.USAGE.status; + } + return ExitStatus.CODE_ERROR.status; + } + } + + public Command parse(String cmdName, String[] args) throws Exception { + CliToolConfig.Cmd cmd = config.cmd(cmdName); + return parse(cmd, args); + } + + public Command parse(CliToolConfig.Cmd cmd, String[] args) throws Exception { + CommandLineParser parser = new GnuParser(); + CommandLine cli = parser.parse(CliToolConfig.OptionsSource.HELP.options(), args, true); + if (cli.hasOption("h")) { + return helpCmd(cmd); + } + cli = parser.parse(cmd.options(), args); + Terminal.Verbosity verbosity = Terminal.Verbosity.resolve(cli); + terminal.verbosity(verbosity); + return parse(cmd.name(), cli); + } + + protected Command.Help helpCmd(CliToolConfig.Cmd cmd) { + return new Command.Help(cmd, terminal); + } + + protected static Command.Exit exitCmd(ExitStatus status) { + return new Command.Exit(null, status, null); + } + + protected static Command.Exit exitCmd(ExitStatus status, Terminal terminal, String msg, Object... args) { + return new Command.Exit(String.format(Locale.ROOT, msg, args), status, terminal); + } + + protected abstract Command parse(String cmdName, CommandLine cli) throws Exception; + + public static abstract class Command { + + protected final Terminal terminal; + + protected Command(Terminal terminal) { + this.terminal = terminal; + } + + public abstract ExitStatus execute(Settings settings, Environment env) throws Exception; + + public static class Help extends Command { + + private final CliToolConfig.Cmd cmd; + + private Help(CliToolConfig.Cmd cmd, Terminal terminal) { + super(terminal); + this.cmd = cmd; + } + + @Override + public ExitStatus execute(Settings settings, Environment env) throws Exception { + cmd.printUsage(terminal); + return ExitStatus.OK; + } + } + + public static class Exit extends Command { + private final String msg; + private final ExitStatus status; + + private Exit(String msg, ExitStatus status, Terminal terminal) { + super(terminal); + this.msg = msg; + this.status = status; + } + + @Override + public ExitStatus execute(Settings settings, Environment env) throws Exception { + if (msg != null) { + if (status != ExitStatus.OK) { + terminal.printError(msg); + } else { + terminal.println(msg); + } + } + return status; + } + + public ExitStatus status() { + return status; + } + } + } + + + +} + diff --git a/src/main/java/org/elasticsearch/common/cli/CliToolConfig.java b/src/main/java/org/elasticsearch/common/cli/CliToolConfig.java new file mode 100644 index 00000000000..450937e9ceb --- /dev/null +++ b/src/main/java/org/elasticsearch/common/cli/CliToolConfig.java @@ -0,0 +1,271 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.cli; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; + +import java.util.Collection; + +/** + * + */ +public class CliToolConfig { + + public static Builder config(String name, Class toolType) { + return new Builder(name, toolType); + } + + private final Class toolType; + private final String name; + private final ImmutableMap cmds; + + private static final HelpPrinter helpPrinter = new HelpPrinter(); + + private CliToolConfig(String name, Class toolType, Cmd[] cmds) { + this.name = name; + this.toolType = toolType; + ImmutableMap.Builder cmdsBuilder = ImmutableMap.builder(); + for (int i = 0; i < cmds.length; i++) { + cmdsBuilder.put(cmds[i].name, cmds[i]); + } + this.cmds = cmdsBuilder.build(); + } + + public boolean isSingle() { + return cmds.size() == 1; + } + + public Cmd single() { + assert isSingle() : "Requesting single command on a multi-command tool"; + return cmds.values().iterator().next(); + } + + public Class toolType() { + return toolType; + } + + public String name() { + return name; + } + + public Collection cmds() { + return cmds.values(); + } + + public Cmd cmd(String name) { + return cmds.get(name); + } + + public void printUsage(Terminal terminal) { + helpPrinter.print(this, terminal); + } + + public static class Builder { + + public static Cmd.Builder cmd(String name, Class cmdType) { + return new Cmd.Builder(name, cmdType); + } + + public static OptionBuilder option(String shortName, String longName) { + return new OptionBuilder(shortName, longName); + } + + public static OptionGroupBuilder optionGroup(boolean required) { + return new OptionGroupBuilder(required); + } + + private final Class toolType; + private final String name; + private Cmd[] cmds; + + private Builder(String name, Class toolType) { + this.name = name; + this.toolType = toolType; + } + + public Builder cmds(Cmd.Builder... cmds) { + this.cmds = new Cmd[cmds.length]; + for (int i = 0; i < cmds.length; i++) { + this.cmds[i] = cmds[i].build(); + this.cmds[i].toolName = name; + } + return this; + } + + public Builder cmds(Cmd... cmds) { + for (int i = 0; i < cmds.length; i++) { + cmds[i].toolName = name; + } + this.cmds = cmds; + return this; + } + + public CliToolConfig build() { + return new CliToolConfig(name, toolType, cmds); + } + } + + public static class Cmd { + + private String toolName; + private final String name; + private final Class cmdType; + private final Options options; + + private Cmd(String name, Class cmdType, Options options) { + this.name = name; + this.cmdType = cmdType; + this.options = options; + OptionsSource.VERBOSITY.populate(options); + } + + public Class cmdType() { + return cmdType; + } + + public String name() { + return name; + } + + public Options options() { + return options; + } + + public void printUsage(Terminal terminal) { + helpPrinter.print(toolName, this, terminal); + } + + public static class Builder { + + private final String name; + private final Class cmdType; + private Options options = new Options(); + + private Builder(String name, Class cmdType) { + this.name = name; + this.cmdType = cmdType; + } + + public Builder options(OptionBuilder... optionBuilder) { + for (int i = 0; i < optionBuilder.length; i++) { + options.addOption(optionBuilder[i].build()); + } + return this; + } + + public Builder optionGroups(OptionGroupBuilder... optionGroupBuilders) { + for (OptionGroupBuilder builder : optionGroupBuilders) { + options.addOptionGroup(builder.build()); + } + return this; + } + + public Cmd build() { + return new Cmd(name, cmdType, options); + } + } + } + + public static class OptionBuilder { + + private final Option option; + + private OptionBuilder(String shortName, String longName) { + option = new Option(shortName, ""); + option.setLongOpt(longName); + option.setArgName(longName); + } + + public OptionBuilder required(boolean required) { + option.setRequired(required); + return this; + } + + public OptionBuilder hasArg(boolean optional) { + option.setOptionalArg(optional); + option.setArgs(1); + return this; + } + + public Option build() { + return option; + } + } + + public static class OptionGroupBuilder { + + private OptionGroup group; + + private OptionGroupBuilder(boolean required) { + group = new OptionGroup(); + group.setRequired(required); + } + + public OptionGroupBuilder options(OptionBuilder... optionBuilders) { + for (OptionBuilder builder : optionBuilders) { + group.addOption(builder.build()); + } + return this; + } + + public OptionGroup build() { + return group; + } + + } + + static abstract class OptionsSource { + + static final OptionsSource HELP = new OptionsSource() { + + @Override + void populate(Options options) { + options.addOption(new OptionBuilder("h", "help").required(false).build()); + } + }; + + static final OptionsSource VERBOSITY = new OptionsSource() { + @Override + void populate(Options options) { + OptionGroup verbosityGroup = new OptionGroup(); + verbosityGroup.setRequired(false); + verbosityGroup.addOption(new OptionBuilder("s", "silent").required(false).build()); + verbosityGroup.addOption(new OptionBuilder("v", "verbose").required(false).build()); + options.addOptionGroup(verbosityGroup); + } + }; + + private Options options; + + Options options() { + if (options == null) { + options = new Options(); + populate(options); + } + return options; + } + + abstract void populate(Options options); + + } +} diff --git a/src/main/java/org/elasticsearch/common/cli/HelpPrinter.java b/src/main/java/org/elasticsearch/common/cli/HelpPrinter.java new file mode 100644 index 00000000000..4f694e9af38 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/cli/HelpPrinter.java @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.cli; + +import org.elasticsearch.common.io.Streams; +import org.elasticsearch.common.util.Callback; + +import java.io.IOException; +import java.io.InputStream; + +/** + * + */ +public class HelpPrinter { + + private static final String HELP_FILE_EXT = ".help"; + + public void print(CliToolConfig config, Terminal terminal) { + print(config.toolType(), config.name(), terminal); + } + + public void print(String toolName, CliToolConfig.Cmd cmd, Terminal terminal) { + print(cmd.cmdType(), toolName + "-" + cmd.name(), terminal); + } + + private static void print(Class clazz, String name, final Terminal terminal) { + terminal.println(Terminal.Verbosity.SILENT); + try (InputStream input = clazz.getResourceAsStream(name + HELP_FILE_EXT)) { + Streams.readAllLines(input, new Callback() { + @Override + public void handle(String line) { + terminal.println(Terminal.Verbosity.SILENT, line); + } + }); + } catch (IOException ioe) { + ioe.printStackTrace(terminal.writer()); + } + terminal.println(); + } +} diff --git a/src/main/java/org/elasticsearch/common/cli/Terminal.java b/src/main/java/org/elasticsearch/common/cli/Terminal.java new file mode 100644 index 00000000000..0c0070f9720 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/cli/Terminal.java @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.cli; + +import org.apache.commons.cli.CommandLine; + +import java.io.*; +import java.util.Locale; + +/** +* +*/ +public abstract class Terminal { + + public static final Terminal DEFAULT = ConsoleTerminal.supported() ? new ConsoleTerminal() : new SystemTerminal(); + + public static enum Verbosity { + SILENT(0), NORMAL(1), VERBOSE(2); + + private final int level; + + private Verbosity(int level) { + this.level = level; + } + + public boolean enabled(Verbosity verbosity) { + return level >= verbosity.level; + } + + public static Verbosity resolve(CommandLine cli) { + if (cli.hasOption("s")) { + return SILENT; + } + if (cli.hasOption("v")) { + return VERBOSE; + } + return NORMAL; + } + } + + private Verbosity verbosity = Verbosity.NORMAL; + + + public Terminal() { + this(Verbosity.NORMAL); + } + + public Terminal(Verbosity verbosity) { + this.verbosity = verbosity; + } + + public void verbosity(Verbosity verbosity) { + this.verbosity = verbosity; + } + + public Verbosity verbosity() { + return verbosity; + } + + public abstract String readText(String text, Object... args); + + public abstract char[] readSecret(String text, Object... args); + + public void println() { + println(Verbosity.NORMAL); + } + + public void println(String msg, Object... args) { + println(Verbosity.NORMAL, msg, args); + } + + public void print(String msg, Object... args) { + print(Verbosity.NORMAL, msg, args); + } + + public void println(Verbosity verbosity) { + println(verbosity, ""); + } + + public void println(Verbosity verbosity, String msg, Object... args) { + print(verbosity, msg + System.lineSeparator(), args); + } + + public void print(Verbosity verbosity, String msg, Object... args) { + if (this.verbosity.enabled(verbosity)) { + doPrint(msg, args); + } + } + + public void printError(String msg, Object... args) { + println(Verbosity.SILENT, "ERROR: " + msg, args); + } + + protected abstract void doPrint(String msg, Object... args); + + public abstract PrintWriter writer(); + + private static class ConsoleTerminal extends Terminal { + + final Console console = System.console(); + + static boolean supported() { + return System.console() != null; + } + + @Override + public void doPrint(String msg, Object... args) { + console.printf(msg, args); + console.flush(); + } + + @Override + public String readText(String text, Object... args) { + return console.readLine(text, args); + } + + @Override + public char[] readSecret(String text, Object... args) { + return console.readPassword(text, args); + } + + @Override + public PrintWriter writer() { + return console.writer(); + } + + } + + private static class SystemTerminal extends Terminal { + + private final PrintWriter printWriter = new PrintWriter(System.out); + + @Override + public void doPrint(String msg, Object... args) { + System.out.print(String.format(Locale.ROOT, msg, args)); + } + + @Override + public String readText(String text, Object... args) { + print(text, args); + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + try { + return reader.readLine(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + @Override + public char[] readSecret(String text, Object... args) { + return readText(text, args).toCharArray(); + } + + @Override + public PrintWriter writer() { + return printWriter; + } + } +} diff --git a/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java b/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java index c4d05dd1ddd..c9649689909 100644 --- a/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java +++ b/src/main/java/org/elasticsearch/common/http/client/HttpDownloadHelper.java @@ -130,7 +130,7 @@ public class HttpDownloadHelper { public static class VerboseProgress implements DownloadProgress { private int dots = 0; // CheckStyle:VisibilityModifier OFF - bc - PrintStream out; + PrintWriter writer; // CheckStyle:VisibilityModifier ON /** @@ -139,14 +139,23 @@ public class HttpDownloadHelper { * @param out the output stream. */ public VerboseProgress(PrintStream out) { - this.out = out; + this.writer = new PrintWriter(out); + } + + /** + * Construct a verbose progress reporter. + * + * @param writer the output stream. + */ + public VerboseProgress(PrintWriter writer) { + this.writer = this.writer; } /** * begin a download */ public void beginDownload() { - out.print("Downloading "); + writer.print("Downloading "); dots = 0; } @@ -154,9 +163,9 @@ public class HttpDownloadHelper { * tick handler */ public void onTick() { - out.print("."); + writer.print("."); if (dots++ > 50) { - out.flush(); + writer.flush(); dots = 0; } } @@ -165,8 +174,8 @@ public class HttpDownloadHelper { * end a download */ public void endDownload() { - out.println("DONE"); - out.flush(); + writer.println("DONE"); + writer.flush(); } } diff --git a/src/main/java/org/elasticsearch/common/io/Streams.java b/src/main/java/org/elasticsearch/common/io/Streams.java index 1d78786fe0a..49a382f555f 100644 --- a/src/main/java/org/elasticsearch/common/io/Streams.java +++ b/src/main/java/org/elasticsearch/common/io/Streams.java @@ -20,10 +20,13 @@ package org.elasticsearch.common.io; import com.google.common.base.Charsets; +import com.google.common.collect.Lists; import org.elasticsearch.common.Preconditions; import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.util.Callback; import java.io.*; +import java.util.List; /** * Simple utility methods for file and stream copying. @@ -297,4 +300,24 @@ public abstract class Streams { } return read; } + + public static List readAllLines(InputStream input) throws IOException { + final List lines = Lists.newArrayList(); + readAllLines(input, new Callback() { + @Override + public void handle(String line) { + lines.add(line); + } + }); + return lines; + } + + public static void readAllLines(InputStream input, Callback callback) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, Charsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + callback.handle(line); + } + } + } } diff --git a/src/main/java/org/elasticsearch/common/util/Callback.java b/src/main/java/org/elasticsearch/common/util/Callback.java new file mode 100644 index 00000000000..d4e3c94f700 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/util/Callback.java @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.util; + +/** + * + */ +public interface Callback { + + void handle(T t); + +} diff --git a/src/test/java/org/elasticsearch/common/cli/CliToolTestCase.java b/src/test/java/org/elasticsearch/common/cli/CliToolTestCase.java new file mode 100644 index 00000000000..8440e57ec99 --- /dev/null +++ b/src/test/java/org/elasticsearch/common/cli/CliToolTestCase.java @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.cli; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.test.ElasticsearchTestCase; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; + +/** + * + */ +public class CliToolTestCase extends ElasticsearchTestCase { + + protected static String[] args(String command) { + if (!Strings.hasLength(command)) { + return Strings.EMPTY_ARRAY; + } + return command.split("\\s+"); + } + + public static class TerminalMock extends Terminal { + + private static final PrintWriter DEV_NULL = new PrintWriter(new DevNullWriter()); + + public TerminalMock() { + super(Verbosity.NORMAL); + } + + public TerminalMock(Verbosity verbosity) { + super(verbosity); + } + + @Override + protected void doPrint(String msg, Object... args) { + } + + @Override + public String readText(String text, Object... args) { + return null; + } + + @Override + public char[] readSecret(String text, Object... args) { + return new char[0]; + } + + @Override + public void print(String msg, Object... args) { + } + + @Override + public PrintWriter writer() { + return DEV_NULL; + } + + private static class DevNullWriter extends Writer { + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + } + + @Override + public void flush() throws IOException { + } + + @Override + public void close() throws IOException { + } + } + } + +} diff --git a/src/test/java/org/elasticsearch/common/cli/CliToolTests.java b/src/test/java/org/elasticsearch/common/cli/CliToolTests.java new file mode 100644 index 00000000000..517061d89f1 --- /dev/null +++ b/src/test/java/org/elasticsearch/common/cli/CliToolTests.java @@ -0,0 +1,343 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.cli; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.cli.CommandLine; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +/** + * + */ +public class CliToolTests extends CliToolTestCase { + + @Test + public void testOK() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) { + executed.set(true); + return CliTool.ExitStatus.OK; + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertExecuted(status, CliTool.ExitStatus.OK, executed, true); + } + + @Test + public void testUsageError() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) { + executed.set(true); + return CliTool.ExitStatus.USAGE; + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertExecuted(status, CliTool.ExitStatus.USAGE, executed, true); + } + + @Test + public void testIOError() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed.set(true); + throw new IOException("io error"); + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertExecuted(status, CliTool.ExitStatus.IO_ERROR, executed, true); + } + + @Test + public void testCodeError() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed.set(true); + throw new Exception("random error"); + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertExecuted(status, CliTool.ExitStatus.CODE_ERROR, executed, true); + } + + @Test + public void testMultiCommand() { + Terminal terminal = new TerminalMock(); + int count = randomIntBetween(2, 7); + final AtomicReference[] executed = new AtomicReference[count]; + for (int i = 0; i < executed.length; i++) { + executed[i] = new AtomicReference<>(false); + } + NamedCommand[] cmds = new NamedCommand[count]; + for (int i = 0; i < count; i++) { + final int index = i; + cmds[i] = new NamedCommand("cmd" + index, terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed[index].set(true); + return CliTool.ExitStatus.OK; + } + }; + } + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int cmdIndex = randomIntBetween(0, count-1); + int status = tool.execute("cmd" + cmdIndex); + assertThat(status, is(CliTool.ExitStatus.OK.status())); + for (int i = 0; i < executed.length; i++) { + assertThat(executed[i].get(), is(i == cmdIndex)); + } + } + + @Test + public void testMultiCommand_UnknownCommand() { + Terminal terminal = new TerminalMock(); + int count = randomIntBetween(2, 7); + final AtomicReference[] executed = new AtomicReference[count]; + for (int i = 0; i < executed.length; i++) { + executed[i] = new AtomicReference<>(false); + } + NamedCommand[] cmds = new NamedCommand[count]; + for (int i = 0; i < count; i++) { + final int index = i; + cmds[i] = new NamedCommand("cmd" + index, terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed[index].set(true); + return CliTool.ExitStatus.OK; + } + }; + } + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int status = tool.execute("cmd" + count); // "cmd" + count doesn't exist + assertThat(status, is(CliTool.ExitStatus.USAGE.status())); + for (int i = 0; i < executed.length; i++) { + assertThat(executed[i].get(), is(false)); + } + } + + @Test + public void testSingleCommand_ToolHelp() throws Exception { + final AtomicInteger writeCounter = new AtomicInteger(0); + Terminal terminal = new TerminalMock() { + @Override + public void doPrint(String msg, Object... args) { + int count = writeCounter.incrementAndGet(); + switch (count) { + case 1: + assertThat(msg, equalTo("\n")); + break; + case 2: + assertThat(msg, equalTo("cmd1 help\n")); + break; + case 3: + assertThat(msg, equalTo("\n")); + break; + default: + fail("written more than expected"); + } + } + }; + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd1", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed.set(true); + throw new IOException("io error"); + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(args("-h")); + assertExecuted(status, CliTool.ExitStatus.OK, writeCounter, 3); + } + + @Test + public void testMultiCommand_ToolHelp() { + final AtomicInteger writeCounter = new AtomicInteger(0); + Terminal terminal = new TerminalMock() { + @Override + public void doPrint(String msg, Object... args) { + int count = writeCounter.incrementAndGet(); + switch (count) { + case 1: + assertThat(msg, equalTo("\n")); + break; + case 2: + assertThat(msg, equalTo("tool help\n")); + break; + case 3: + assertThat(msg, equalTo("\n")); + break; + default: + fail("written more than expected"); + } + } + }; + NamedCommand[] cmds = new NamedCommand[2]; + cmds[0] = new NamedCommand("cmd0", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + cmds[1] = new NamedCommand("cmd1", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int status = tool.execute(args("-h")); + assertExecuted(status, CliTool.ExitStatus.OK, writeCounter, 3); + } + + @Test + public void testMultiCommand_CmdHelp() { + final AtomicInteger writeCounter = new AtomicInteger(0); + Terminal terminal = new TerminalMock() { + @Override + public void doPrint(String msg, Object... args) { + int count = writeCounter.incrementAndGet(); + switch (count) { + case 1: + assertThat(msg, equalTo("\n")); + break; + case 2: + assertThat(msg, equalTo("cmd1 help\n")); + break; + case 3: + assertThat(msg, equalTo("\n")); + break; + default: + fail("written more than expected"); + } + } + }; + NamedCommand[] cmds = new NamedCommand[2]; + cmds[0] = new NamedCommand("cmd0", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + cmds[1] = new NamedCommand("cmd1", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int status = tool.execute(args("cmd1 -h")); + assertExecuted(status, CliTool.ExitStatus.OK, writeCounter, 3); + } + + private static void assertExecuted(int actualStatus, CliTool.ExitStatus expectedStatus, AtomicReference actualExecuted, boolean expectedExecuted) { + assertThat(actualExecuted.get(), is(expectedExecuted)); + assertThat(actualStatus, is(expectedStatus.status())); + } + + private static void assertExecuted(int actualStatus, CliTool.ExitStatus expectedStatus, AtomicInteger actualExecuted, int expectedExecuted) { + assertThat(actualExecuted.get(), is(expectedExecuted)); + assertThat(actualStatus, is(expectedStatus.status())); + } + + private static class SingleCmdTool extends CliTool { + + private final Command command; + + private SingleCmdTool(String name, Terminal terminal, NamedCommand command) { + super(CliToolConfig.config(name, SingleCmdTool.class) + .cmds(cmd(command.name, command.getClass())) + .build(), terminal); + this.command = command; + } + + @Override + protected Command parse(String cmdName, CommandLine cli) throws Exception { + return command; + } + } + + private static class MultiCmdTool extends CliTool { + + private final Map commands; + + private MultiCmdTool(String name, Terminal terminal, NamedCommand... commands) { + super(CliToolConfig.config(name, MultiCmdTool.class) + .cmds(cmds(commands)) + .build(), terminal); + ImmutableMap.Builder commandByName = ImmutableMap.builder(); + for (int i = 0; i < commands.length; i++) { + commandByName.put(commands[i].name, commands[i]); + } + this.commands = commandByName.build(); + } + + @Override + protected Command parse(String cmdName, CommandLine cli) throws Exception { + return commands.get(cmdName); + } + + private static CliToolConfig.Cmd[] cmds(NamedCommand... commands) { + CliToolConfig.Cmd[] cmds = new CliToolConfig.Cmd[commands.length]; + for (int i = 0; i < commands.length; i++) { + cmds[i] = cmd(commands[i].name, commands[i].getClass()).build(); + } + return cmds; + } + } + + private static abstract class NamedCommand extends CliTool.Command { + + private final String name; + + private NamedCommand(String name, Terminal terminal) { + super(terminal); + this.name = name; + } + } + + +} diff --git a/src/test/java/org/elasticsearch/common/cli/TerminalTests.java b/src/test/java/org/elasticsearch/common/cli/TerminalTests.java new file mode 100644 index 00000000000..0d60f217945 --- /dev/null +++ b/src/test/java/org/elasticsearch/common/cli/TerminalTests.java @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.cli; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.*; + +/** + * + */ +public class TerminalTests extends CliToolTestCase { + + @Test + public void testVerbosity() throws Exception { + final AtomicReference printed = new AtomicReference<>(false); + + Terminal terminal = new TerminalMock(Terminal.Verbosity.SILENT) { + @Override + protected void doPrint(String msg, Object... args) { + printed.set(true); + assertThat(msg, equalTo("text")); + } + }; + terminal.print(Terminal.Verbosity.SILENT, "text"); + assertPrinted(printed); + terminal.print(Terminal.Verbosity.NORMAL, "text"); + assertNotPrinted(printed); + terminal.print(Terminal.Verbosity.VERBOSE, "text"); + assertNotPrinted(printed); + + terminal = new TerminalMock(Terminal.Verbosity.NORMAL) { + @Override + protected void doPrint(String msg, Object... args) { + printed.set(true); + assertThat(msg, equalTo("text")); + } + }; + terminal.print(Terminal.Verbosity.SILENT, "text"); + assertPrinted(printed); + terminal.print(Terminal.Verbosity.NORMAL, "text"); + assertPrinted(printed); + terminal.print(Terminal.Verbosity.VERBOSE, "text"); + assertNotPrinted(printed); + + terminal = new TerminalMock(Terminal.Verbosity.VERBOSE) { + @Override + protected void doPrint(String msg, Object... args) { + printed.set(true); + assertThat(msg, equalTo("text")); + } + }; + terminal.print(Terminal.Verbosity.SILENT, "text"); + assertPrinted(printed); + terminal.print(Terminal.Verbosity.NORMAL, "text"); + assertPrinted(printed); + terminal.print(Terminal.Verbosity.VERBOSE, "text"); + assertPrinted(printed); + } + + private static void assertPrinted(AtomicReference actual) { + assertThat(actual.get(), is(true)); + actual.set(false); // resetting + } + + private static void assertNotPrinted(AtomicReference actual) { + assertThat(actual.get(), is(false)); + } + +} diff --git a/src/test/resources/org/elasticsearch/common/cli/tool-cmd1.help b/src/test/resources/org/elasticsearch/common/cli/tool-cmd1.help new file mode 100644 index 00000000000..d083e3a6534 --- /dev/null +++ b/src/test/resources/org/elasticsearch/common/cli/tool-cmd1.help @@ -0,0 +1 @@ +cmd1 help \ No newline at end of file diff --git a/src/test/resources/org/elasticsearch/common/cli/tool.help b/src/test/resources/org/elasticsearch/common/cli/tool.help new file mode 100644 index 00000000000..023b1accdff --- /dev/null +++ b/src/test/resources/org/elasticsearch/common/cli/tool.help @@ -0,0 +1 @@ +tool help \ No newline at end of file