/*
* Copyright © 2012-2015 Cask Data, Inc.
*
* 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 co.cask.cdap.cli;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import co.cask.cdap.cli.command.system.HelpCommand;
import co.cask.cdap.cli.command.system.SearchCommandsCommand;
import co.cask.cdap.cli.commandset.DefaultCommands;
import co.cask.cdap.cli.completer.supplier.EndpointSupplier;
import co.cask.cdap.cli.util.FilePathResolver;
import co.cask.cdap.cli.util.InstanceURIParser;
import co.cask.cdap.cli.util.table.AltStyleTableRenderer;
import co.cask.cdap.cli.util.table.TableRenderer;
import co.cask.cdap.client.config.ClientConfig;
import co.cask.cdap.client.config.ConnectionConfig;
import co.cask.cdap.client.exception.DisconnectedException;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.utils.OSDetector;
import co.cask.common.cli.CLI;
import co.cask.common.cli.Command;
import co.cask.common.cli.CommandSet;
import co.cask.common.cli.exception.CLIExceptionHandler;
import co.cask.common.cli.exception.InvalidCommandException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import jline.console.completer.Completer;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ConnectException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLHandshakeException;
/**
* Main class for the CDAP CLI.
*/
public class CLIMain {
private static final boolean DEFAULT_VERIFY_SSL = true;
private static final boolean DEFAULT_AUTOCONNECT = true;
private static final String TOOL_NAME = "cdap-cli";
@VisibleForTesting
public static final Option HELP_OPTION = new Option(
"h", "help", false, "Print the usage message.");
@VisibleForTesting
public static final Option URI_OPTION = new Option(
"u", "uri", true, "CDAP instance URI to interact with in" +
" the format \"[http[s]://]<hostname>[:<port>[/<namespace>]]\"." +
" Defaults to \"" + getDefaultURI().toString() + "\".");
@VisibleForTesting
public static final Option VERIFY_SSL_OPTION = new Option(
"v", "verify-ssl", true, "If \"true\", verify SSL certificate when making requests." +
" Defaults to \"" + DEFAULT_VERIFY_SSL + "\".");
@VisibleForTesting
public static final Option AUTOCONNECT_OPTION = new Option(
"a", "autoconnect", true, "If \"true\", try provided connection" +
" (from " + URI_OPTION.getLongOpt() + ")" +
" upon launch or try default connection if none provided." +
" Defaults to \"" + DEFAULT_AUTOCONNECT + "\".");
@VisibleForTesting
public static final Option DEBUG_OPTION = new Option(
"d", "debug", false, "Print exception stack traces.");
private static final Option SCRIPT_OPTION = new Option(
"s", "script", true, "Execute a file containing a series of CLI commands, line-by-line.");
private final CLI cli;
private final Iterable<CommandSet<Command>> commands;
private final CLIConfig cliConfig;
private final Injector injector;
private final LaunchOptions options;
private final FilePathResolver filePathResolver;
public CLIMain(final LaunchOptions options,
final CLIConfig cliConfig) throws URISyntaxException, IOException {
this.options = options;
this.cliConfig = cliConfig;
injector = Guice.createInjector(
new AbstractModule() {
@Override
protected void configure() {
bind(LaunchOptions.class).toInstance(options);
bind(CConfiguration.class).toInstance(CConfiguration.create());
bind(PrintStream.class).toInstance(cliConfig.getOutput());
bind(CLIConfig.class).toInstance(cliConfig);
bind(ClientConfig.class).toInstance(cliConfig.getClientConfig());
}
}
);
this.commands = ImmutableList.of(
injector.getInstance(DefaultCommands.class),
new CommandSet<>(ImmutableList.<Command>of(
new HelpCommand(getCommandsSupplier(), cliConfig),
new SearchCommandsCommand(getCommandsSupplier(), cliConfig)
)));
filePathResolver = injector.getInstance(FilePathResolver.class);
Map<String, Completer> completers = injector.getInstance(DefaultCompleters.class).get();
cli = new CLI<>(Iterables.concat(commands), completers);
cli.setExceptionHandler(new CLIExceptionHandler<Exception>() {
@Override
public boolean handleException(PrintStream output, Exception e, int timesRetried) {
if (e instanceof SSLHandshakeException) {
output.printf("To ignore this error, set \"--%s false\" when starting the CLI\n",
VERIFY_SSL_OPTION.getLongOpt());
} else if (e instanceof InvalidCommandException) {
InvalidCommandException ex = (InvalidCommandException) e;
output.printf("Invalid command '%s'. Enter 'help' for a list of commands\n", ex.getInput());
} else if (e instanceof DisconnectedException || e instanceof ConnectException) {
cli.getReader().setPrompt("cdap (DISCONNECTED)> ");
} else {
output.println("Error: " + e.getMessage());
}
if (options.isDebug()) {
e.printStackTrace(output);
}
return false;
}
});
cli.addCompleterSupplier(injector.getInstance(EndpointSupplier.class));
cli.getReader().setExpandEvents(false);
cliConfig.addHostnameChangeListener(new CLIConfig.ConnectionChangeListener() {
@Override
public void onConnectionChanged(CLIConnectionConfig config) {
updateCLIPrompt(config);
}
});
}
/**
* Tries to autoconnect to the provided URI in options.
*/
public boolean tryAutoconnect(CommandLine command) {
if (!options.isAutoconnect()) {
return true;
}
InstanceURIParser instanceURIParser = injector.getInstance(InstanceURIParser.class);
try {
CLIConnectionConfig connection = instanceURIParser.parse(options.getUri());
cliConfig.tryConnect(connection, options.isVerifySSL(), cliConfig.getOutput(), options.isDebug());
return true;
} catch (Exception e) {
if (options.isDebug()) {
e.printStackTrace(cliConfig.getOutput());
} else {
cliConfig.getOutput().println(e.getMessage());
}
if (!command.hasOption(URI_OPTION.getOpt())) {
cliConfig.getOutput().printf("Specify the CDAP instance URI with the -u command line argument.\n");
}
return false;
}
}
public static URI getDefaultURI() {
return ConnectionConfig.DEFAULT.getURI();
}
public FilePathResolver getFilePathResolver() {
return filePathResolver;
}
private void updateCLIPrompt(CLIConnectionConfig config) {
cli.getReader().setPrompt(getPrompt(config));
}
public String getPrompt(CLIConnectionConfig config) {
try {
return "cdap (" + config.getURI().resolve("/" + config.getNamespace()) + ")> ";
} catch (DisconnectedException e) {
return "cdap (DISCONNECTED)> ";
}
}
public TableRenderer getTableRenderer() {
return cliConfig.getTableRenderer();
}
public CLI getCLI() {
return this.cli;
}
public Supplier<Iterable<CommandSet<Command>>> getCommandsSupplier() {
return new Supplier<Iterable<CommandSet<Command>>>() {
@Override
public Iterable<CommandSet<Command>> get() {
return commands;
}
};
}
public static void main(String[] args) {
// disable logback logging
Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.OFF);
final PrintStream output = System.out;
Options options = getOptions();
CLIMainArgs cliMainArgs = CLIMainArgs.parse(args, options);
CommandLineParser parser = new BasicParser();
try {
CommandLine command = parser.parse(options, cliMainArgs.getOptionTokens());
if (command.hasOption(HELP_OPTION.getOpt())) {
usage();
System.exit(0);
}
LaunchOptions launchOptions = LaunchOptions.builder()
.setUri(command.getOptionValue(URI_OPTION.getOpt(), getDefaultURI().toString()))
.setDebug(command.hasOption(DEBUG_OPTION.getOpt()))
.setVerifySSL(parseBooleanOption(command, VERIFY_SSL_OPTION, DEFAULT_VERIFY_SSL))
.setAutoconnect(parseBooleanOption(command, AUTOCONNECT_OPTION, DEFAULT_AUTOCONNECT))
.build();
String scriptFile = command.getOptionValue(SCRIPT_OPTION.getOpt(), "");
boolean hasScriptFile = command.hasOption(SCRIPT_OPTION.getOpt());
String[] commandArgs = cliMainArgs.getCommandTokens();
try {
ClientConfig clientConfig = ClientConfig.builder()
.setConnectionConfig(null)
.setDefaultReadTimeout(60 * 1000)
.build();
final CLIConfig cliConfig = new CLIConfig(clientConfig, output, new AltStyleTableRenderer());
CLIMain cliMain = new CLIMain(launchOptions, cliConfig);
CLI cli = cliMain.getCLI();
if (!cliMain.tryAutoconnect(command)) {
System.exit(0);
}
CLIConnectionConfig connectionConfig = new CLIConnectionConfig(
cliConfig.getClientConfig().getConnectionConfig(),
cliConfig.getCurrentNamespace(), null);
cliMain.updateCLIPrompt(connectionConfig);
if (hasScriptFile) {
File script = cliMain.getFilePathResolver().resolvePathToFile(scriptFile);
if (!script.exists()) {
output.println("ERROR: Script file '" + script.getAbsolutePath() + "' does not exist");
System.exit(1);
}
List<String> scriptLines = Files.readLines(script, Charsets.UTF_8);
for (String scriptLine : scriptLines) {
output.print(cliMain.getPrompt(connectionConfig));
output.println(scriptLine);
cli.execute(scriptLine, output);
output.println();
}
} else if (commandArgs.length == 0) {
cli.startInteractiveMode(output);
} else {
cli.execute(Joiner.on(" ").join(commandArgs), output);
}
} catch (DisconnectedException e) {
output.printf("Couldn't reach the CDAP instance at '%s'.", e.getConnectionConfig().getURI().toString());
} catch (Exception e) {
e.printStackTrace(output);
}
} catch (ParseException e) {
output.println(e.getMessage());
usage();
}
}
private static boolean parseBooleanOption(CommandLine command, Option option, boolean defaultValue) {
String value = command.getOptionValue(option.getOpt(), Boolean.toString(defaultValue));
return "true".equals(value);
}
@VisibleForTesting
public static Options getOptions() {
Options options = new Options();
addOptionalOption(options, HELP_OPTION);
addOptionalOption(options, URI_OPTION);
addOptionalOption(options, VERIFY_SSL_OPTION);
addOptionalOption(options, AUTOCONNECT_OPTION);
addOptionalOption(options, DEBUG_OPTION);
addOptionalOption(options, SCRIPT_OPTION);
return options;
}
private static void addOptionalOption(Options options, Option option) {
OptionGroup optionalGroup = new OptionGroup();
optionalGroup.setRequired(false);
optionalGroup.addOption(option);
options.addOptionGroup(optionalGroup);
}
private static void usage() {
String toolName = TOOL_NAME + (OSDetector.isWindows() ? ".bat" : ".sh");
HelpFormatter formatter = new HelpFormatter();
String args =
"[--autoconnect <true|false>] " +
"[--debug] " +
"[--help] " +
"[--verify-ssl <true|false>] " +
"[--uri <uri>]" +
"[--script <script-file>]";
formatter.printHelp(toolName + " " + args, getOptions());
System.exit(0);
}
}