diff --git a/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java b/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java index cba3dce013a..93a410b7d21 100644 --- a/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java +++ b/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java @@ -21,20 +21,20 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith("With[{}]")); assertThat(readLine(), startsWith("\\_Project[[?*]]")); assertThat(readLine(), startsWith(" \\_UnresolvedRelation[[index=test],null,Unknown index [test]]")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN " + (randomBoolean() ? "" : "(PLAN ANALYZED) ") + "SELECT * FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); assertThat(readLine(), startsWith("Project[[test_field{r}#")); assertThat(readLine(), startsWith("\\_SubQueryAlias[test]")); assertThat(readLine(), startsWith(" \\_EsRelation[test][test_field{r}#")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN OPTIMIZED) SELECT * FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); assertThat(readLine(), startsWith("Project[[test_field{r}#")); assertThat(readLine(), startsWith("\\_EsRelation[test][test_field{r}#")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); // TODO in this case we should probably remove the source filtering entirely. Right? It costs but we don't need it. assertThat(command("EXPLAIN (PLAN EXECUTABLE) SELECT * FROM test"), containsString("plan")); @@ -47,7 +47,7 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith(" \"excludes\" : [ ]")); assertThat(readLine(), startsWith(" }")); assertThat(readLine(), startsWith("}]")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } public void testExplainWithWhere() throws IOException { @@ -60,7 +60,7 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith("\\_Project[[?*]]")); assertThat(readLine(), startsWith(" \\_Filter[?i = 2]")); assertThat(readLine(), startsWith(" \\_UnresolvedRelation[[index=test],null,Unknown index [test]]")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN " + (randomBoolean() ? "" : "(PLAN ANALYZED) ") + "SELECT * FROM test WHERE i = 2"), containsString("plan")); @@ -69,14 +69,14 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith("\\_Filter[i{r}#")); assertThat(readLine(), startsWith(" \\_SubQueryAlias[test]")); assertThat(readLine(), startsWith(" \\_EsRelation[test][i{r}#")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN OPTIMIZED) SELECT * FROM test WHERE i = 2"), containsString("plan")); assertThat(readLine(), startsWith("----------")); assertThat(readLine(), startsWith("Project[[i{r}#")); assertThat(readLine(), startsWith("\\_Filter[i{r}#")); assertThat(readLine(), startsWith(" \\_EsRelation[test][i{r}#")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN EXECUTABLE) SELECT * FROM test WHERE i = 2"), containsString("plan")); assertThat(readLine(), startsWith("----------")); @@ -99,7 +99,7 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith(" \"i\"")); assertThat(readLine(), startsWith(" ]")); assertThat(readLine(), startsWith("}]")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } public void testExplainWithCount() throws IOException { @@ -111,7 +111,7 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith("With[{}]")); assertThat(readLine(), startsWith("\\_Project[[?COUNT(?*)]]")); assertThat(readLine(), startsWith(" \\_UnresolvedRelation[[index=test],null,Unknown index [test]]")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN " + (randomBoolean() ? "" : "(PLAN ANALYZED) ") + "SELECT COUNT(*) FROM test"), containsString("plan")); @@ -119,13 +119,13 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith("Aggregate[[],[COUNT(1)#")); assertThat(readLine(), startsWith("\\_SubQueryAlias[test]")); assertThat(readLine(), startsWith(" \\_EsRelation[test][i{r}#")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN OPTIMIZED) SELECT COUNT(*) FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); assertThat(readLine(), startsWith("Aggregate[[],[COUNT(1)#")); assertThat(readLine(), startsWith("\\_EsRelation[test][i{r}#")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); assertThat(command("EXPLAIN (PLAN EXECUTABLE) SELECT COUNT(*) FROM test"), containsString("plan")); assertThat(readLine(), startsWith("----------")); @@ -134,6 +134,6 @@ public class CliExplainIT extends CliIntegrationTestCase { assertThat(readLine(), startsWith(" \"_source\" : false,")); assertThat(readLine(), startsWith(" \"stored_fields\" : \"_none_\"")); assertThat(readLine(), startsWith("}]")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } } diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java index 6fd9e600e28..fd6022739da 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java @@ -34,7 +34,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { assertEquals("---------------+---------------+---------------", cli.readLine()); assertThat(cli.readLine(), containsString("1 |2 |3")); assertThat(cli.readLine(), containsString("4 |5 |6")); - assertEquals("[0m", cli.readLine()); + assertEquals("", cli.readLine()); } } @@ -91,7 +91,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { for (Map.Entry column : columns.entrySet()) { assertThat(cli.readLine(), both(startsWith(column.getKey())).and(containsString("|" + column.getValue()))); } - assertEquals("[0m", cli.readLine()); + assertEquals("", cli.readLine()); } } @@ -103,7 +103,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { for (String table : tables) { assertThat(cli.readLine(), containsString(table)); } - assertEquals("[0m", cli.readLine()); + assertEquals("", cli.readLine()); } } diff --git a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/SelectTestCase.java b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/SelectTestCase.java index 09b6c64e436..3add41db6d5 100644 --- a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/SelectTestCase.java +++ b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/SelectTestCase.java @@ -17,7 +17,7 @@ public abstract class SelectTestCase extends CliIntegrationTestCase { assertThat(command("SELECT * FROM test"), containsString("test_field")); assertThat(readLine(), containsString("----------")); assertThat(readLine(), containsString("test_value")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } public void testSelectWithWhere() throws IOException { @@ -26,6 +26,6 @@ public abstract class SelectTestCase extends CliIntegrationTestCase { assertThat(command("SELECT * FROM test WHERE i = 2"), RegexMatcher.matches("\\s*i\\s*\\|\\s*test_field\\s*")); assertThat(readLine(), containsString("----------")); assertThat(readLine(), RegexMatcher.matches("\\s*2\\s*\\|\\s*test_value2\\s*")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } } diff --git a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java index 7d61044d2ab..2e12b44df1a 100644 --- a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java +++ b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java @@ -20,7 +20,7 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { assertThat(readLine(), containsString("----------")); assertThat(readLine(), RegexMatcher.matches("\\s*test[12]\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*test[12]\\s*")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } public void testShowFunctions() throws IOException { @@ -39,7 +39,7 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { while (scalarFunction.matcher(line).matches()) { line = readLine(); } - assertEquals("[0m", line); + assertEquals("", line); } public void testShowFunctionsLikePrefix() throws IOException { @@ -47,7 +47,7 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { assertThat(readLine(), containsString("----------")); assertThat(readLine(), RegexMatcher.matches("\\s*LOG\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*LOG10\\s*\\|\\s*SCALAR\\s*")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } public void testShowFunctionsLikeInfix() throws IOException { @@ -59,6 +59,6 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_YEAR\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*HOUR_OF_DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*MINUTE_OF_DAY\\s*\\|\\s*SCALAR\\s*")); - assertEquals("[0m", readLine()); + assertEquals("", readLine()); } } diff --git a/sql/cli/build.gradle b/sql/cli/build.gradle index e731f087d1b..415c9479505 100644 --- a/sql/cli/build.gradle +++ b/sql/cli/build.gradle @@ -9,6 +9,7 @@ dependencies { compile project(':x-pack-elasticsearch:sql:shared-client') compile project(':x-pack-elasticsearch:sql:cli-proto') compile project(':x-pack-elasticsearch:sql:shared-proto') + compile project(':core:cli') runtime "org.fusesource.jansi:jansi:1.16" runtime "org.elasticsearch:jna:4.4.0-1" @@ -19,10 +20,12 @@ dependencyLicenses { mapping from: /cli-proto.*/, to: 'elasticsearch' mapping from: /shared-client.*/, to: 'elasticsearch' mapping from: /shared-proto.*/, to: 'elasticsearch' + mapping from: /elasticsearch-cli.*/, to: 'elasticsearch' mapping from: /jackson-.*/, to: 'jackson' ignoreSha 'cli-proto' ignoreSha 'shared-client' ignoreSha 'shared-proto' + ignoreSha 'elasticsearch-cli' } forbiddenApisMain { diff --git a/sql/cli/licenses/jopt-simple-5.0.2.jar.sha1 b/sql/cli/licenses/jopt-simple-5.0.2.jar.sha1 new file mode 100644 index 00000000000..b50ed4fea3b --- /dev/null +++ b/sql/cli/licenses/jopt-simple-5.0.2.jar.sha1 @@ -0,0 +1 @@ +98cafc6081d5632b61be2c9e60650b64ddbc637c \ No newline at end of file diff --git a/sql/cli/licenses/jopt-simple-LICENSE.txt b/sql/cli/licenses/jopt-simple-LICENSE.txt new file mode 100644 index 00000000000..85f923a9526 --- /dev/null +++ b/sql/cli/licenses/jopt-simple-LICENSE.txt @@ -0,0 +1,24 @@ +/* + The MIT License + + Copyright (c) 2004-2015 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ diff --git a/sql/cli/licenses/jopt-simple-NOTICE.txt b/sql/cli/licenses/jopt-simple-NOTICE.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java index c187e7a7424..ade585ae2ad 100644 --- a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java @@ -5,350 +5,82 @@ */ package org.elasticsearch.xpack.sql.cli; -import org.elasticsearch.xpack.sql.cli.net.protocol.QueryResponse; -import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration; -import org.elasticsearch.xpack.sql.client.shared.SuppressForbidden; -import org.elasticsearch.xpack.sql.client.shared.JreHttpUrlConnection; -import org.elasticsearch.xpack.sql.client.shared.StringUtils; -import org.elasticsearch.xpack.sql.protocol.shared.AbstractQueryInitRequest; -import org.jline.reader.EndOfFileException; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; -import org.jline.reader.UserInterruptException; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; -import org.jline.utils.AttributedString; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.InfoCmp.Capability; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import org.elasticsearch.cli.Command; +import org.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.Terminal; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.xpack.sql.cli.command.ClearScreenCliCommand; +import org.elasticsearch.xpack.sql.cli.command.CliCommand; +import org.elasticsearch.xpack.sql.cli.command.CliCommands; +import org.elasticsearch.xpack.sql.cli.command.CliSession; +import org.elasticsearch.xpack.sql.cli.command.FetchSeparatorCliCommand; +import org.elasticsearch.xpack.sql.cli.command.FetchSizeCliCommand; +import org.elasticsearch.xpack.sql.cli.command.PrintLogoCommand; +import org.elasticsearch.xpack.sql.cli.command.ServerInfoCliCommand; +import org.elasticsearch.xpack.sql.cli.command.ServerQueryCliCommand; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.sql.SQLException; -import java.util.Locale; -import java.util.Properties; +import java.util.Arrays; +import java.util.List; import java.util.logging.LogManager; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import static org.elasticsearch.xpack.sql.client.shared.UriUtils.parseURI; -import static org.elasticsearch.xpack.sql.client.shared.UriUtils.removeQuery; -import static org.jline.utils.AttributedStyle.BOLD; -import static org.jline.utils.AttributedStyle.BRIGHT; -import static org.jline.utils.AttributedStyle.DEFAULT; -import static org.jline.utils.AttributedStyle.RED; -import static org.jline.utils.AttributedStyle.YELLOW; +public class Cli extends Command { + private final OptionSpec debugOption; + private final OptionSpec connectionString; -public class Cli { - public static String DEFAULT_CONNECTION_STRING = "http://localhost:9200/"; - public static URI DEFAULT_URI = URI.create(DEFAULT_CONNECTION_STRING); + public Cli() { + super("Elasticsearch SQL CLI", Cli::configureLogging); + this.debugOption = parser.acceptsAll(Arrays.asList("d", "debug"), + "Enable debug logging") + .withRequiredArg().ofType(Boolean.class) + .defaultsTo(Boolean.parseBoolean(System.getProperty("cli.debug", "false"))); + this.connectionString = parser.nonOptions("uri"); + } - public static void main(String... args) throws Exception { - /* Initialize the logger from the a properties file we bundle. This makes sure - * we get useful error messages from jLine. */ - LogManager.getLogManager().readConfiguration(Cli.class.getResourceAsStream("/logging.properties")); - final URI uri; - final String connectionString; - Properties properties = new Properties(); - String user = null; - String password = null; - if (args.length > 0) { - connectionString = args[0]; - try { - uri = removeQuery(parseURI(connectionString, DEFAULT_URI), connectionString, DEFAULT_URI); - } catch (IllegalArgumentException ex) { - exit(ex.getMessage(), 1); - return; - } - user = uri.getUserInfo(); - if (user != null) { - int colonIndex = user.indexOf(':'); - if (colonIndex >= 0) { - password = user.substring(colonIndex + 1); - user = user.substring(0, colonIndex); - } - } - } else { - uri = DEFAULT_URI; - connectionString = DEFAULT_CONNECTION_STRING; - } - - try (Terminal term = TerminalBuilder.builder().build()) { - try { - if (user != null) { - if (password == null) { - term.writer().print("password: "); - term.writer().flush(); - term.echo(false); - password = new BufferedReader(term.reader()).readLine(); - term.echo(true); - } - properties.setProperty(ConnectionConfiguration.AUTH_USER, user); - properties.setProperty(ConnectionConfiguration.AUTH_PASS, password); - } - - boolean debug = StringUtils.parseBoolean(System.getProperty("cli.debug", "false")); - Cli console = new Cli(debug, new ConnectionConfiguration(uri, connectionString, properties), term); - console.run(); - } catch (FatalException e) { - term.writer().println(e.getMessage()); - } + public static void main(String[] args) throws Exception { + final Cli cli = new Cli(); + int status = cli.main(args, Terminal.DEFAULT); + if (status != ExitCodes.OK) { + exit(status); } } - private final boolean debug; - private final Terminal term; - private final CliHttpClient cliClient; - private int fetchSize = AbstractQueryInitRequest.DEFAULT_FETCH_SIZE; - private String fetchSeparator = ""; - - Cli(boolean debug, ConnectionConfiguration cfg, Terminal terminal) { - this.debug = debug; - term = terminal; - cliClient = new CliHttpClient(cfg); - } - - void run() throws IOException { - PrintWriter out = term.writer(); - - LineReader reader = LineReaderBuilder.builder() - .terminal(term) - .completer(Completers.INSTANCE) - .build(); - - String DEFAULT_PROMPT = new AttributedString("sql> ", DEFAULT.foreground(YELLOW)).toAnsi(term); - String MULTI_LINE_PROMPT = new AttributedString(" | ", DEFAULT.foreground(YELLOW)).toAnsi(term); - - StringBuilder multiLine = new StringBuilder(); - String prompt = DEFAULT_PROMPT; - - out.flush(); - printLogo(); - - while (true) { - String line = null; - try { - line = reader.readLine(prompt); - } catch (UserInterruptException ex) { - // ignore - } catch (EndOfFileException ex) { - return; - } - - if (line == null) { - continue; - } - line = line.trim(); - - if (!line.endsWith(";")) { - multiLine.append(" "); - multiLine.append(line); - prompt = MULTI_LINE_PROMPT; - continue; - } - - line = line.substring(0, line.length() - 1); - - prompt = DEFAULT_PROMPT; - if (multiLine.length() > 0) { - // append the line without trailing ; - multiLine.append(line); - line = multiLine.toString().trim(); - multiLine.setLength(0); - } - - // special case to handle exit - if (isExit(line)) { - out.println(new AttributedString("Bye!", DEFAULT.foreground(BRIGHT)).toAnsi(term)); - out.flush(); - return; - } - boolean wasLocal = handleLocalCommand(line); - if (false == wasLocal) { - try { - if (isServerInfo(line)) { - executeServerInfo(); - } else { - executeQuery(line); - } - } catch (RuntimeException e) { - handleExceptionWhileCommunicatingWithServer(e); - } - out.println(); - } - - out.flush(); - } - } - - /** - * Handle an exception while communication with the server. Extracted - * into a method so that tests can bubble the failure. - */ - protected void handleExceptionWhileCommunicatingWithServer(RuntimeException e) { - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append("Communication error [", BOLD.foreground(RED)); - asb.append(e.getMessage(), DEFAULT.boldOff().italic().foreground(YELLOW)); - asb.append("]", BOLD.underlineOff().foreground(RED)); - term.writer().println(asb.toAnsi(term)); - if (debug) { - e.printStackTrace(term.writer()); - } - } - - private void printLogo() { - term.puts(Capability.clear_screen); - try (InputStream in = Cli.class.getResourceAsStream("/logo.txt")) { - if (in == null) { - throw new FatalException("Could not find logo!"); - } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - term.writer().println(line); - } - } - } catch (IOException e) { - throw new FatalException("Could not load logo!", e); - } - - term.writer().println(); - } - - private static final Pattern LOGO_PATTERN = Pattern.compile("logo", Pattern.CASE_INSENSITIVE); - private static final Pattern CLEAR_PATTERN = Pattern.compile("cls", Pattern.CASE_INSENSITIVE); - private static final Pattern FETCH_SIZE_PATTERN = Pattern.compile("fetch(?: |_)size *= *(.+)", Pattern.CASE_INSENSITIVE); - private static final Pattern FETCH_SEPARATOR_PATTERN = Pattern.compile("fetch(?: |_)separator *= *\"(.+)\"", Pattern.CASE_INSENSITIVE); - private boolean handleLocalCommand(String line) { - Matcher m = LOGO_PATTERN.matcher(line); - if (m.matches()) { - printLogo(); - return true; - } - m = CLEAR_PATTERN.matcher(line); - if (m.matches()) { - term.puts(Capability.clear_screen); - return true; - } - m = FETCH_SIZE_PATTERN.matcher(line); - if (m.matches()) { - int proposedFetchSize; - try { - proposedFetchSize = fetchSize = Integer.parseInt(m.group(1)); - } catch (NumberFormatException e) { - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append("Invalid fetch size [", BOLD.foreground(RED)); - asb.append(m.group(1), DEFAULT.boldOff().italic().foreground(YELLOW)); - asb.append("]", BOLD.underlineOff().foreground(RED)); - term.writer().println(asb.toAnsi(term)); - return true; - } - if (proposedFetchSize <= 0) { - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append("Invalid fetch size [", BOLD.foreground(RED)); - asb.append(m.group(1), DEFAULT.boldOff().italic().foreground(YELLOW)); - asb.append("]. Must be > 0.", BOLD.underlineOff().foreground(RED)); - term.writer().println(asb.toAnsi(term)); - return true; - } - this.fetchSize = proposedFetchSize; - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append("fetch size set to ", DEFAULT); - asb.append(Integer.toString(fetchSize), DEFAULT.foreground(BRIGHT)); - term.writer().println(asb.toAnsi(term)); - return true; - } - m = FETCH_SEPARATOR_PATTERN.matcher(line); - if (m.matches()) { - fetchSeparator = m.group(1); - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append("fetch separator set to \"", DEFAULT); - asb.append(fetchSeparator, DEFAULT.foreground(BRIGHT)); - asb.append("\"", DEFAULT); - term.writer().println(asb.toAnsi(term)); - return true; - } - - return false; - } - - private boolean isServerInfo(String line) { - line = line.toLowerCase(Locale.ROOT); - return line.equals("info"); - } - - private void executeServerInfo() { + private static void configureLogging() { try { - term.writer().println(ResponseToString.toAnsi(cliClient.serverInfo()).toAnsi(term)); - } catch (SQLException e) { - error("Error fetching server info", e.getMessage()); + /* Initialize the logger from the a properties file we bundle. This makes sure + * we get useful error messages from jLine. */ + LogManager.getLogManager().readConfiguration(Cli.class.getResourceAsStream("/logging.properties")); + } catch (IOException ex) { + throw new RuntimeException("cannot setup logging", ex); } } - private static boolean isExit(String line) { - line = line.toLowerCase(Locale.ROOT); - return line.equals("exit") || line.equals("quit"); + @Override + protected void execute(org.elasticsearch.cli.Terminal terminal, OptionSet options) throws Exception { + boolean debug = debugOption.value(options); + List args = connectionString.values(options); + if (args.size() > 1) { + throw new UserException(ExitCodes.USAGE, "expecting a single uri"); + } + execute(args.size() == 1 ? args.get(0) : null, debug); } - private void executeQuery(String line) throws IOException { - QueryResponse response; - try { - response = cliClient.queryInit(line, fetchSize); - } catch (SQLException e) { - if (JreHttpUrlConnection.SQL_STATE_BAD_SERVER.equals(e.getSQLState())) { - error("Server error", e.getMessage()); - } else { - error("Bad request", e.getMessage()); - } - return; + private void execute(String uri, boolean debug) throws Exception { + CliCommand cliCommand = new CliCommands( + new PrintLogoCommand(), + new ClearScreenCliCommand(), + new FetchSizeCliCommand(), + new FetchSeparatorCliCommand(), + new ServerInfoCliCommand(), + new ServerQueryCliCommand() + ); + try (CliTerminal cliTerminal = new JLineTerminal()) { + ConnectionBuilder connectionBuilder = new ConnectionBuilder(cliTerminal); + CliSession cliSession = new CliSession(new CliHttpClient(connectionBuilder.buildConnection(uri))); + cliSession.setDebug(debug); + new CliRepl(cliTerminal, cliSession, cliCommand).execute(); } - while (true) { - term.writer().print(ResponseToString.toAnsi(response).toAnsi(term)); - term.writer().flush(); - if (response.cursor().isEmpty()) { - // Successfully finished the entire query! - return; - } - if (false == fetchSeparator.equals("")) { - term.writer().println(fetchSeparator); - } - try { - response = cliClient.nextPage(response.cursor()); - } catch (SQLException e) { - if (JreHttpUrlConnection.SQL_STATE_BAD_SERVER.equals(e.getSQLState())) { - error("Server error", e.getMessage()); - } else { - error("Bad request", e.getMessage()); - } return; - } - } - } - - private void error(String type, String message) { - AttributedStringBuilder sb = new AttributedStringBuilder(); - sb.append(type + " [", BOLD.foreground(RED)); - sb.append(message, DEFAULT.boldOff().italic().foreground(YELLOW)); - sb.append("]", BOLD.underlineOff().foreground(RED)); - term.writer().print(sb.toAnsi(term)); - } - - static class FatalException extends RuntimeException { - FatalException(String message, Throwable cause) { - super(message, cause); - } - - FatalException(String message) { - super(message); - } - } - - @SuppressForbidden(reason = "CLI application") - private static void exit(String message, int code) { - System.err.println(message); - System.exit(code); } } diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliException.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliException.java deleted file mode 100644 index 86616eaecd2..00000000000 --- a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliException.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.cli; - -import java.util.Locale; - -import static java.lang.String.format; - -@SuppressWarnings("serial") -public class CliException extends RuntimeException { - - public CliException() { - super(); - } - - public CliException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - - public CliException(String message, Object... args) { - super(format(Locale.ROOT, message, args)); - } - - public CliException(Throwable cause, String message, Object... args) { - super(format(Locale.ROOT, message, args), cause); - } - - public CliException(Throwable cause) { - super(cause); - } - -} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliRepl.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliRepl.java new file mode 100644 index 00000000000..9fe4dece230 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliRepl.java @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +import org.elasticsearch.xpack.sql.cli.command.CliCommand; +import org.elasticsearch.xpack.sql.cli.command.CliSession; + +import java.util.Locale; + +public class CliRepl { + + private CliTerminal cliTerminal; + private CliCommand cliCommand; + private CliSession cliSession; + + public CliRepl(CliTerminal cliTerminal, CliSession cliSession, CliCommand cliCommand) { + this.cliTerminal = cliTerminal; + this.cliCommand = cliCommand; + this.cliSession = cliSession; + } + + public void execute() { + String DEFAULT_PROMPT = "sql> "; + String MULTI_LINE_PROMPT = " | "; + + StringBuilder multiLine = new StringBuilder(); + String prompt = DEFAULT_PROMPT; + + cliTerminal.flush(); + cliCommand.handle(cliTerminal, cliSession, "logo"); + + while (true) { + String line = cliTerminal.readLine(prompt); + if (line == null) { + return; + } + line = line.trim(); + + if (!line.endsWith(";")) { + multiLine.append(" "); + multiLine.append(line); + prompt = MULTI_LINE_PROMPT; + continue; + } + + line = line.substring(0, line.length() - 1); + + prompt = DEFAULT_PROMPT; + if (multiLine.length() > 0) { + // append the line without trailing ; + multiLine.append(line); + line = multiLine.toString().trim(); + multiLine.setLength(0); + } + + // special case to handle exit + if (isExit(line)) { + cliTerminal.line().em("Bye!").ln(); + cliTerminal.flush(); + return; + } + if (cliCommand.handle(cliTerminal, cliSession, line) == false) { + cliTerminal.error("Unrecognized command", line); + } + cliTerminal.println(); + } + } + + private static boolean isExit(String line) { + line = line.toLowerCase(Locale.ROOT); + return line.equals("exit") || line.equals("quit"); + } + +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliTerminal.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliTerminal.java new file mode 100644 index 00000000000..017a7598ae6 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliTerminal.java @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +import java.io.IOException; + +/** + * Represents a terminal endpoint + */ +public interface CliTerminal extends AutoCloseable { + + /** + * Prints line with plain text + */ + void print(String text); + + /** + * Prints line with plain text followed by a new line + */ + void println(String text); + + /** + * Prints a formatted error message + */ + void error(String type, String message); + + /** + * Prints a new line + */ + void println(); + + /** + * Clears the terminal + */ + void clear(); + + /** + * Flushes the terminal + */ + void flush(); + + /** + * Prints the stacktrace of the exception + */ + void printStackTrace(Exception ex); + + /** + * Prompts the user to enter the password and returns it. + */ + String readPassword(String prompt); + + /** + * Reads the line from the terminal. + */ + String readLine(String prompt); + + /** + * Creates a new line builder, which allows building a formatted lines. + * + * The line is not displayed until it is closed with ln() or end(). + */ + LineBuilder line(); + + interface LineBuilder { + /** + * Adds a plain text to the line + */ + LineBuilder text(String text); + + /** + * Adds a text with emphasis to the line + */ + LineBuilder em(String text); + + /** + * Adds a text representing the error message + */ + LineBuilder error(String text); + + /** + * Adds a text representing a parameter of the error message + */ + LineBuilder param(String text); + + /** + * Adds '\n' to the line and send it to the screen. + */ + void ln(); + + /** + * Sends the line to the screen. + */ + void end(); + } + + @Override + void close() throws IOException; +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilder.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilder.java new file mode 100644 index 00000000000..2f1df4cd9ea --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilder.java @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +import org.elasticsearch.cli.UserException; +import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration; + +import java.net.URI; +import java.util.Properties; + +import static org.elasticsearch.xpack.sql.client.shared.UriUtils.parseURI; +import static org.elasticsearch.xpack.sql.client.shared.UriUtils.removeQuery; + +/** + * Connection Builder. Can interactively ask users for the password if it is not provided + */ +public class ConnectionBuilder { + public static String DEFAULT_CONNECTION_STRING = "http://localhost:9200/"; + public static URI DEFAULT_URI = URI.create(DEFAULT_CONNECTION_STRING); + + private CliTerminal cliTerminal; + + public ConnectionBuilder(CliTerminal cliTerminal) { + this.cliTerminal = cliTerminal; + } + + public ConnectionConfiguration buildConnection(String arg) throws UserException { + final URI uri; + final String connectionString; + Properties properties = new Properties(); + String user = null; + String password = null; + if (arg != null) { + connectionString = arg; + uri = removeQuery(parseURI(connectionString, DEFAULT_URI), connectionString, DEFAULT_URI); + user = uri.getUserInfo(); + if (user != null) { + int colonIndex = user.indexOf(':'); + if (colonIndex >= 0) { + password = user.substring(colonIndex + 1); + user = user.substring(0, colonIndex); + } + } + } else { + uri = DEFAULT_URI; + connectionString = DEFAULT_CONNECTION_STRING; + } + + if (user != null) { + if (password == null) { + password = cliTerminal.readPassword("password: "); + } + properties.setProperty(ConnectionConfiguration.AUTH_USER, user); + properties.setProperty(ConnectionConfiguration.AUTH_PASS, password); + } + + return new ConnectionConfiguration(uri, connectionString, properties); + } + +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/FatalCliException.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/FatalCliException.java new file mode 100644 index 00000000000..c314ac1009e --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/FatalCliException.java @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +/** + * Throwing this except will cause the CLI to terminate + */ +public class FatalCliException extends RuntimeException { + public FatalCliException(String message, Throwable cause) { + super(message, cause); + } + + public FatalCliException(String message) { + super(message); + } +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/JLineTerminal.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/JLineTerminal.java new file mode 100644 index 00000000000..dc8fa73b0b9 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/JLineTerminal.java @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.InfoCmp; + +import java.io.BufferedReader; +import java.io.IOException; + +import static org.jline.utils.AttributedStyle.BOLD; +import static org.jline.utils.AttributedStyle.BRIGHT; +import static org.jline.utils.AttributedStyle.DEFAULT; +import static org.jline.utils.AttributedStyle.RED; +import static org.jline.utils.AttributedStyle.YELLOW; + +/** + * jline-based implementation of the terminal + */ +public class JLineTerminal implements CliTerminal { + + private Terminal terminal; + private LineReader reader; + + protected JLineTerminal() { + try { + this.terminal = TerminalBuilder.builder().build(); + reader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(Completers.INSTANCE) + .build(); + } catch (IOException ex) { + throw new FatalCliException("Cannot use terminal", ex); + } + } + + @Override + public LineBuilder line() { + return new LineBuilder(); + } + + @Override + public void print(String text) { + terminal.writer().print(text); + } + + @Override + public void println(String text) { + terminal.writer().println(text); + } + + @Override + public void error(String type, String message) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.append(type + " [", BOLD.foreground(RED)); + sb.append(message, DEFAULT.boldOff().italic().foreground(YELLOW)); + sb.append("]", BOLD.underlineOff().foreground(RED)); + terminal.writer().print(sb.toAnsi(terminal)); + terminal.flush(); + } + + @Override + public void println() { + terminal.writer().println(); + } + + @Override + public void clear() { + terminal.puts(InfoCmp.Capability.clear_screen); + } + + @Override + public void flush() { + terminal.flush(); + } + + @Override + public void printStackTrace(Exception ex) { + ex.printStackTrace(terminal.writer()); + } + + @Override + public String readPassword(String prompt) { + terminal.writer().print(prompt); + terminal.writer().flush(); + terminal.echo(false); + try { + return new BufferedReader(terminal.reader()).readLine(); + } catch (IOException ex) { + throw new FatalCliException("Error reading password", ex); + } finally { + terminal.echo(true); + } + } + + @Override + public String readLine(String prompt) { + try { + String attributedString = new AttributedString(prompt, DEFAULT.foreground(YELLOW)).toAnsi(terminal); + return reader.readLine(attributedString); + } catch (UserInterruptException ex) { + return ""; + } catch (EndOfFileException ex) { + return null; + } + } + + @Override + public void close() throws IOException { + terminal.close(); + } + + public final class LineBuilder implements CliTerminal.LineBuilder { + AttributedStringBuilder line; + + private LineBuilder() { + line = new AttributedStringBuilder(); + } + + public LineBuilder text(String text) { + line.append(text, DEFAULT); + return this; + } + + public LineBuilder em(String text) { + line.append(text, DEFAULT.foreground(BRIGHT)); + return this; + } + + + public LineBuilder error(String text) { + line.append(text, BOLD.foreground(RED)); + return this; + } + + public LineBuilder param(String text) { + line.append(text, DEFAULT.italic().foreground(YELLOW)); + return this; + } + + public void ln() { + terminal.writer().println(line.toAnsi(terminal)); + } + + public void end() { + terminal.writer().print(line.toAnsi(terminal)); + terminal.writer().flush(); + } + } + +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ResponseToString.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ResponseToString.java deleted file mode 100644 index 01a1a2ac55c..00000000000 --- a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ResponseToString.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.cli; - -import org.elasticsearch.xpack.sql.cli.net.protocol.InfoResponse; -import org.elasticsearch.xpack.sql.cli.net.protocol.Proto.ResponseType; -import org.elasticsearch.xpack.sql.cli.net.protocol.QueryResponse; -import org.elasticsearch.xpack.sql.protocol.shared.Response; -import org.jline.utils.AttributedStringBuilder; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.jline.utils.AttributedStyle.BRIGHT; -import static org.jline.utils.AttributedStyle.DEFAULT; -import static org.jline.utils.AttributedStyle.WHITE; - -abstract class ResponseToString { - - static AttributedStringBuilder toAnsi(Response response) { - AttributedStringBuilder sb = new AttributedStringBuilder(); - - switch ((ResponseType) response.responseType()) { - case QUERY_INIT: - case QUERY_PAGE: - QueryResponse cmd = (QueryResponse) response; - if (cmd.data != null) { - String data = cmd.data.toString(); - if (data.startsWith("digraph ")) { - sb.append(handleGraphviz(data), DEFAULT.foreground(WHITE)); - } - else { - sb.append(data, DEFAULT.foreground(WHITE)); - } - } - return sb; - case INFO: - InfoResponse info = (InfoResponse) response; - sb.append("Node:", DEFAULT.foreground(BRIGHT)); - sb.append(info.node, DEFAULT.foreground(WHITE)); - sb.append(" Cluster:", DEFAULT.foreground(BRIGHT)); - sb.append(info.cluster, DEFAULT.foreground(WHITE)); - sb.append(" Version:", DEFAULT.foreground(BRIGHT)); - sb.append(info.versionString, DEFAULT.foreground(WHITE)); - return sb; - default: - throw new IllegalArgumentException("Unsupported response: " + response); - } - } - - private static String handleGraphviz(String str) { - try { - // save the content to a temp file - Path dotTempFile = Files.createTempFile(Paths.get("."), "sql-gv", ".dot"); - Files.write(dotTempFile, str.getBytes(StandardCharsets.UTF_8)); - // run graphviz on it (dot needs to be on the file path) - //Desktop desktop = Desktop.getDesktop(); - //File f = dotTempFile.toFile(); - //desktop.open(f); - //f.deleteOnExit(); - return "Saved graph file at " + dotTempFile; - - } catch (IOException ex) { - return "Cannot save graph file; " + ex.getMessage(); - } - } -} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/AbstractCliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/AbstractCliCommand.java new file mode 100644 index 00000000000..f7efc0888d3 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/AbstractCliCommand.java @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The base class for simple commands that match the pattern + */ +public abstract class AbstractCliCommand implements CliCommand { + + protected final Pattern pattern; + + AbstractCliCommand(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean handle(CliTerminal terminal, CliSession cliSession, String line) { + Matcher matcher = pattern.matcher(line); + if (matcher.matches()) { + return doHandle(terminal, cliSession, matcher, line); + } + return false; + } + + /** + * the perform the command + * returns true if the command handled the line and false otherwise + */ + protected abstract boolean doHandle(CliTerminal terminal, CliSession cliSession, Matcher m, String line); +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/AbstractServerCliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/AbstractServerCliCommand.java new file mode 100644 index 00000000000..bee868bdac2 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/AbstractServerCliCommand.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; + +public abstract class AbstractServerCliCommand implements CliCommand { + + public AbstractServerCliCommand() { + } + + @Override + public final boolean handle(CliTerminal terminal, CliSession cliSession, String line) { + try { + return doHandle(terminal, cliSession, line); + } catch (RuntimeException e) { + handleExceptionWhileCommunicatingWithServer(terminal, cliSession, e); + } + return true; + } + + protected abstract boolean doHandle(CliTerminal cliTerminal, CliSession cliSession, String line); + + /** + * Handle an exception while communication with the server. Extracted + * into a method so that tests can bubble the failure. + */ + protected void handleExceptionWhileCommunicatingWithServer(CliTerminal terminal, CliSession cliSession, RuntimeException e) { + terminal.line().error("Communication error [").param(e.getMessage()).error("]").ln(); + if (cliSession.isDebug()) { + terminal.printStackTrace(e); + } + } + + +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ClearScreenCliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ClearScreenCliCommand.java new file mode 100644 index 00000000000..ffde1ec556a --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ClearScreenCliCommand.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * cls command that cleans the screen + */ +public class ClearScreenCliCommand extends AbstractCliCommand { + + public ClearScreenCliCommand() { + super(Pattern.compile("cls", Pattern.CASE_INSENSITIVE)); + } + + @Override + protected boolean doHandle(CliTerminal terminal, CliSession cliSession, Matcher m, String line) { + terminal.clear(); + return true; + } +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliCommand.java new file mode 100644 index 00000000000..b87b06b3803 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliCommand.java @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; + +public interface CliCommand { + + /** + * Handle the command, return true if the command is handled, false otherwise + */ + boolean handle(CliTerminal terminal, CliSession cliSession, String line); + +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliCommands.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliCommands.java new file mode 100644 index 00000000000..192195c5b22 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliCommands.java @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; + +import java.util.Arrays; +import java.util.List; + +/** + * Wrapper for several commands + */ +public class CliCommands implements CliCommand { + + private final List commands; + + public CliCommands(CliCommand... commands) { + this.commands = Arrays.asList(commands); + } + + @Override + public boolean handle(CliTerminal terminal, CliSession cliSession, String line) { + for (CliCommand cliCommand : commands) { + if (cliCommand.handle(terminal, cliSession, line)) { + return true; + } + } + return false; + } +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliSession.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliSession.java new file mode 100644 index 00000000000..1bc24ed5c11 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/CliSession.java @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliHttpClient; +import org.elasticsearch.xpack.sql.protocol.shared.AbstractQueryInitRequest; + +/** + * Stores information about the current session + */ +public class CliSession { + private final CliHttpClient cliHttpClient; + private int fetchSize = AbstractQueryInitRequest.DEFAULT_FETCH_SIZE; + private String fetchSeparator = ""; + private boolean debug; + + public CliSession(CliHttpClient cliHttpClient) { + this.cliHttpClient = cliHttpClient; + } + + public CliHttpClient getClient() { + return cliHttpClient; + } + + public void setFetchSize(int fetchSize) { + if (fetchSize <= 0) { + throw new IllegalArgumentException("Must be > 0."); + } + this.fetchSize = fetchSize; + } + + public int getFetchSize() { + return fetchSize; + } + + public void setFetchSeparator(String fetchSeparator) { + this.fetchSeparator = fetchSeparator; + } + + public String getFetchSeparator() { + return fetchSeparator; + } + + public void setDebug(boolean debug) { + this.debug = debug; + } + + public boolean isDebug() { + return debug; + } +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/FetchSeparatorCliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/FetchSeparatorCliCommand.java new file mode 100644 index 00000000000..786f31cb010 --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/FetchSeparatorCliCommand.java @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * fetch_separator command that allows to change the separator string between fetches + */ +public class FetchSeparatorCliCommand extends AbstractCliCommand { + + public FetchSeparatorCliCommand() { + super(Pattern.compile("fetch(?: |_)separator *= *\"(.+)\"", Pattern.CASE_INSENSITIVE)); + } + + @Override + protected boolean doHandle(CliTerminal terminal, CliSession cliSession, Matcher m, String line) { + cliSession.setFetchSeparator(m.group(1)); + terminal.line().text("fetch separator set to \"").em(cliSession.getFetchSeparator()).text("\"").end(); + return true; + } +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/FetchSizeCliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/FetchSizeCliCommand.java new file mode 100644 index 00000000000..8ccef47e19d --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/FetchSizeCliCommand.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * fetch_size command that allows to change the size of fetches + */ +public class FetchSizeCliCommand extends AbstractCliCommand { + + public FetchSizeCliCommand() { + super(Pattern.compile("fetch(?: |_)size *= *(.+)", Pattern.CASE_INSENSITIVE)); + } + + @Override + protected boolean doHandle(CliTerminal terminal, CliSession cliSession, Matcher m, String line) { + try { + cliSession.setFetchSize(Integer.parseInt(m.group(1))); + } catch (NumberFormatException e) { + terminal.line().error("Invalid fetch size [").param(m.group(1)).error("]").end(); + return true; + } catch (IllegalArgumentException e) { + terminal.line().error("Invalid fetch size [").param(m.group(1)).error("]. " + e.getMessage()).end(); + return true; + } + terminal.line().text("fetch size set to ").em(Integer.toString(cliSession.getFetchSize())).end(); + return true; + } +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/PrintLogoCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/PrintLogoCommand.java new file mode 100644 index 00000000000..306189b535a --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/PrintLogoCommand.java @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.Cli; +import org.elasticsearch.xpack.sql.cli.CliTerminal; +import org.elasticsearch.xpack.sql.cli.FatalCliException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * logo command that cleans the screen and prints the logo + */ +public class PrintLogoCommand extends AbstractCliCommand { + + public PrintLogoCommand() { + super(Pattern.compile("logo", Pattern.CASE_INSENSITIVE)); + } + + @Override + protected boolean doHandle(CliTerminal terminal, CliSession cliSession, Matcher m, String line) { + printLogo(terminal); + return true; + } + + public void printLogo(CliTerminal terminal) { + terminal.clear(); + try (InputStream in = Cli.class.getResourceAsStream("/logo.txt")) { + if (in == null) { + throw new FatalCliException("Could not find logo!"); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + terminal.println(line); + } + } + } catch (IOException e) { + throw new FatalCliException("Could not load logo!", e); + } + + terminal.println(); + } + +} \ No newline at end of file diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ServerInfoCliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ServerInfoCliCommand.java new file mode 100644 index 00000000000..cc4830a40fd --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ServerInfoCliCommand.java @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; +import org.elasticsearch.xpack.sql.cli.net.protocol.InfoResponse; + +import java.sql.SQLException; +import java.util.Locale; + +public class ServerInfoCliCommand extends AbstractServerCliCommand { + + public ServerInfoCliCommand() { + } + + @Override + public boolean doHandle(CliTerminal terminal, CliSession cliSession, String line) { + if (false == "info".equals(line.toLowerCase(Locale.ROOT))) { + return false; + } + InfoResponse info; + try { + info = cliSession.getClient().serverInfo(); + } catch (SQLException e) { + terminal.error("Error fetching server info", e.getMessage()); + return true; + } + terminal.line() + .text("Node:").em(info.node) + .text(" Cluster:").em(info.cluster) + .text(" Version:").em(info.versionString) + .ln(); + return true; + } +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ServerQueryCliCommand.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ServerQueryCliCommand.java new file mode 100644 index 00000000000..2be15ea7f8c --- /dev/null +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/command/ServerQueryCliCommand.java @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.xpack.sql.cli.CliTerminal; +import org.elasticsearch.xpack.sql.cli.net.protocol.QueryResponse; +import org.elasticsearch.xpack.sql.client.shared.JreHttpUrlConnection; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.SQLException; + +public class ServerQueryCliCommand extends AbstractServerCliCommand { + + @Override + protected boolean doHandle(CliTerminal terminal, CliSession cliSession, String line) { + QueryResponse response; + try { + response = cliSession.getClient().queryInit(line, cliSession.getFetchSize()); + } catch (SQLException e) { + if (JreHttpUrlConnection.SQL_STATE_BAD_SERVER.equals(e.getSQLState())) { + terminal.error("Server error", e.getMessage()); + } else { + terminal.error("Bad request", e.getMessage()); + } + return true; + } + if (response.data.startsWith("digraph ")) { + handleGraphviz(terminal, response.data); + return true; + } + while (true) { + handleText(terminal, response.data); + if (response.cursor().isEmpty()) { + // Successfully finished the entire query! + terminal.flush(); + return true; + } + if (false == cliSession.getFetchSeparator().equals("")) { + terminal.println(cliSession.getFetchSeparator()); + } + try { + response = cliSession.getClient().nextPage(response.cursor()); + } catch (SQLException e) { + if (JreHttpUrlConnection.SQL_STATE_BAD_SERVER.equals(e.getSQLState())) { + terminal.error("Server error", e.getMessage()); + } else { + terminal.error("Bad request", e.getMessage()); + } + return true; + } + } + } + + private void handleText(CliTerminal terminal, String str) { + terminal.print(str); + } + + private void handleGraphviz(CliTerminal terminal, String str) { + try { + // save the content to a temp file + Path dotTempFile = Files.createTempFile(Paths.get("."), "sql-gv", ".dot"); + Files.write(dotTempFile, str.getBytes(StandardCharsets.UTF_8)); + terminal.println("Saved graph file at " + dotTempFile); + } catch (IOException ex) { + terminal.error("Cannot save graph file ", ex.getMessage()); + } + } + +} diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/CliReplTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/CliReplTests.java new file mode 100644 index 00000000000..2397418256a --- /dev/null +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/CliReplTests.java @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.cli.command.CliCommand; +import org.elasticsearch.xpack.sql.cli.command.CliSession; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class CliReplTests extends ESTestCase { + + public void testBasicCliFunctionality() throws Exception { + CliTerminal cliTerminal = new TestTerminal( + "test;", + "notest;", + "exit;" + ); + CliSession mockSession = mock(CliSession.class); + CliCommand mockCommand = mock(CliCommand.class); + when(mockCommand.handle(cliTerminal, mockSession, "logo")).thenReturn(true); + when(mockCommand.handle(cliTerminal, mockSession, "test")).thenReturn(true); + when(mockCommand.handle(cliTerminal, mockSession, "notest")).thenReturn(false); + + CliRepl cli = new CliRepl(cliTerminal, mockSession, mockCommand); + cli.execute(); + + verify(mockCommand, times(1)).handle(cliTerminal, mockSession, "test"); + verify(mockCommand, times(1)).handle(cliTerminal, mockSession, "logo"); + verify(mockCommand, times(1)).handle(cliTerminal, mockSession, "notest"); + verifyNoMoreInteractions(mockCommand, mockSession); + } + + + public void testFatalCliExceptionHandling() throws Exception { + CliTerminal cliTerminal = new TestTerminal( + "test;", + "fail;" + ); + + CliSession mockSession = mock(CliSession.class); + CliCommand mockCommand = mock(CliCommand.class); + when(mockCommand.handle(cliTerminal, mockSession, "logo")).thenReturn(true); + when(mockCommand.handle(cliTerminal, mockSession, "test")).thenReturn(true); + when(mockCommand.handle(cliTerminal, mockSession, "fail")).thenThrow(new FatalCliException("die")); + + CliRepl cli = new CliRepl(cliTerminal, mockSession, mockCommand); + expectThrows(FatalCliException.class, cli::execute); + + verify(mockCommand, times(1)).handle(cliTerminal, mockSession, "logo"); + verify(mockCommand, times(1)).handle(cliTerminal, mockSession, "test"); + verify(mockCommand, times(1)).handle(cliTerminal, mockSession, "fail"); + verifyNoMoreInteractions(mockCommand, mockSession); + } + +} diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilderTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilderTests.java new file mode 100644 index 00000000000..aadf597d07f --- /dev/null +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilderTests.java @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration; + +import java.net.URI; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class ConnectionBuilderTests extends ESTestCase { + + public void testDefaultConnection() throws Exception { + CliTerminal testTerminal = mock(CliTerminal.class); + ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); + ConnectionConfiguration con = connectionBuilder.buildConnection(null); + assertNull(con.authUser()); + assertNull(con.authPass()); + assertEquals("http://localhost:9200/", con.connectionString()); + assertEquals(URI.create("http://localhost:9200/"), con.baseUri()); + assertEquals(30000, con.connectTimeout()); + assertEquals(60000, con.networkTimeout()); + assertEquals(45000, con.pageTimeout()); + assertEquals(90000, con.queryTimeout()); + assertEquals(1000, con.pageSize()); + verifyNoMoreInteractions(testTerminal); + } + + public void testBasicConnection() throws Exception { + CliTerminal testTerminal = mock(CliTerminal.class); + ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); + ConnectionConfiguration con = connectionBuilder.buildConnection("http://foobar:9242/"); + assertNull(con.authUser()); + assertNull(con.authPass()); + assertEquals("http://foobar:9242/", con.connectionString()); + assertEquals(URI.create("http://foobar:9242/"), con.baseUri()); + verifyNoMoreInteractions(testTerminal); + } + + public void testUserAndPasswordConnection() throws Exception { + CliTerminal testTerminal = mock(CliTerminal.class); + ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); + ConnectionConfiguration con = connectionBuilder.buildConnection("http://user:pass@foobar:9242/"); + assertEquals("user", con.authUser()); + assertEquals("pass", con.authPass()); + assertEquals("http://user:pass@foobar:9242/", con.connectionString()); + assertEquals(URI.create("http://foobar:9242/"), con.baseUri()); + verifyNoMoreInteractions(testTerminal); + } + + public void testUserInteractiveConnection() throws Exception { + CliTerminal testTerminal = mock(CliTerminal.class); + when(testTerminal.readPassword("password: ")).thenReturn("password"); + ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); + ConnectionConfiguration con = connectionBuilder.buildConnection("http://user@foobar:9242/"); + assertEquals("user", con.authUser()); + assertEquals("password", con.authPass()); + assertEquals("http://user@foobar:9242/", con.connectionString()); + assertEquals(URI.create("http://foobar:9242/"), con.baseUri()); + verify(testTerminal, times(1)).readPassword(any()); + verifyNoMoreInteractions(testTerminal); + } +} diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ResponseToStringTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ResponseToStringTests.java deleted file mode 100644 index fa065cc3a53..00000000000 --- a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ResponseToStringTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.cli; - -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.sql.cli.net.protocol.QueryInitResponse; -import org.elasticsearch.xpack.sql.cli.net.protocol.QueryPageResponse; -import org.jline.terminal.Terminal; -import org.jline.utils.AttributedStringBuilder; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ResponseToStringTests extends ESTestCase { - public void testQueryInitResponse() { - AttributedStringBuilder s = ResponseToString.toAnsi(new QueryInitResponse(123, "", "some command response")); - assertEquals("some command response", unstyled(s)); - assertEquals("[37msome command response[0m", fullyStyled(s)); - } - - public void testQueryPageResponse() { - AttributedStringBuilder s = ResponseToString.toAnsi(new QueryPageResponse(123, "", "some command response")); - assertEquals("some command response", unstyled(s)); - assertEquals("[37msome command response[0m", fullyStyled(s)); - } - - private String unstyled(AttributedStringBuilder s) { - Terminal dumb = mock(Terminal.class); - when(dumb.getType()).thenReturn(Terminal.TYPE_DUMB); - return s.toAnsi(dumb); - } - - private String fullyStyled(AttributedStringBuilder s) { - return s - // toAnsi without an argument returns fully styled - .toAnsi() - // replace the escape character because they do not show up in the exception message - .replace("\u001B", ""); - } -} diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/TestTerminal.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/TestTerminal.java new file mode 100644 index 00000000000..697b62fefbb --- /dev/null +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/TestTerminal.java @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestTerminal implements CliTerminal { + + private StringBuilder stringBuilder = new StringBuilder(); + private boolean closed = false; + private Iterator inputLines; + + public TestTerminal(String ... inputLines) { + this.inputLines = Arrays.asList(inputLines).iterator(); + } + + @Override + public LineBuilder line() { + return new LineBuilder() { + + @Override + public LineBuilder text(String text) { + stringBuilder.append(text); + return this; + } + + @Override + public LineBuilder em(String text) { + stringBuilder.append("").append(text).append(""); + return this; + } + + @Override + public LineBuilder error(String text) { + stringBuilder.append("").append(text).append(""); + return this; + } + + @Override + public LineBuilder param(String text) { + stringBuilder.append("").append(text).append(""); + return this; + } + + @Override + public void ln() { + stringBuilder.append("\n"); + } + + @Override + public void end() { + stringBuilder.append(""); + } + }; + } + + @Override + public void print(String text) { + stringBuilder.append(text); + } + + @Override + public void println(String text) { + stringBuilder.append(text); + stringBuilder.append("\n"); + } + + @Override + public void error(String type, String message) { + stringBuilder.append("").append(type).append(" ["); + stringBuilder.append("").append(message).append(""); + stringBuilder.append("]\n"); + } + + @Override + public void println() { + stringBuilder.append("\n"); + } + + @Override + public void clear() { + stringBuilder = new StringBuilder(); + } + + @Override + public void flush() { + stringBuilder.append(""); + } + + @Override + public void printStackTrace(Exception ex) { + stringBuilder.append(""); + } + + @Override + public String readPassword(String prompt) { + return "password"; + } + + @Override + public String readLine(String prompt) { + assertTrue(inputLines.hasNext()); + return inputLines.next(); + } + + @Override + public void close() throws IOException { + assertFalse(closed); + closed = true; + } + + @Override + public String toString() { + return stringBuilder.toString(); + } +} diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/BuiltinCommandTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/BuiltinCommandTests.java new file mode 100644 index 00000000000..99696b24888 --- /dev/null +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/BuiltinCommandTests.java @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.cli.CliHttpClient; +import org.elasticsearch.xpack.sql.cli.TestTerminal; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; + + +public class BuiltinCommandTests extends ESTestCase { + + public void testInvalidCommand() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient cliHttpClient = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(cliHttpClient); + assertFalse(new ClearScreenCliCommand().handle(testTerminal, cliSession, "something")); + assertFalse(new FetchSeparatorCliCommand().handle(testTerminal, cliSession, "something")); + assertFalse(new FetchSizeCliCommand().handle(testTerminal, cliSession, "something")); + assertFalse(new PrintLogoCommand().handle(testTerminal, cliSession, "something")); + verifyNoMoreInteractions(cliHttpClient); + } + + public void testClearScreen() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient cliHttpClient = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(cliHttpClient); + testTerminal.print("not clean"); + assertTrue(new ClearScreenCliCommand().handle(testTerminal, cliSession, "cls")); + assertEquals("", testTerminal.toString()); + verifyNoMoreInteractions(cliHttpClient); + } + + public void testFetchSeparator() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient cliHttpClient = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(cliHttpClient); + FetchSeparatorCliCommand cliCommand = new FetchSeparatorCliCommand(); + assertFalse(cliCommand.handle(testTerminal, cliSession, "fetch")); + assertEquals("", cliSession.getFetchSeparator()); + + assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_separator = \"foo\"")); + assertEquals("foo", cliSession.getFetchSeparator()); + assertEquals("fetch separator set to \"foo\"", testTerminal.toString()); + testTerminal.clear(); + + assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_separator=\"bar\"")); + assertEquals("bar", cliSession.getFetchSeparator()); + assertEquals("fetch separator set to \"bar\"", testTerminal.toString()); + testTerminal.clear(); + + assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch separator=\"baz\"")); + assertEquals("baz", cliSession.getFetchSeparator()); + assertEquals("fetch separator set to \"baz\"", testTerminal.toString()); + verifyNoMoreInteractions(cliHttpClient); + } + + public void testFetchSize() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient cliHttpClient = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(cliHttpClient); + FetchSizeCliCommand cliCommand = new FetchSizeCliCommand(); + assertFalse(cliCommand.handle(testTerminal, cliSession, "fetch")); + assertEquals(1000L, cliSession.getFetchSize()); + + assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_size = \"foo\"")); + assertEquals(1000L, cliSession.getFetchSize()); + assertEquals("Invalid fetch size [\"foo\"]", testTerminal.toString()); + testTerminal.clear(); + + assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_size = 10")); + assertEquals(10L, cliSession.getFetchSize()); + assertEquals("fetch size set to 10", testTerminal.toString()); + + testTerminal.clear(); + + assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_size = -10")); + assertEquals(10L, cliSession.getFetchSize()); + assertEquals("Invalid fetch size [-10]. Must be > 0.", testTerminal.toString()); + verifyNoMoreInteractions(cliHttpClient); + } + + public void testPrintLogo() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient cliHttpClient = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(cliHttpClient); + testTerminal.print("not clean"); + assertTrue(new PrintLogoCommand().handle(testTerminal, cliSession, "logo")); + assertThat(testTerminal.toString(), containsString("SQL")); + verifyNoMoreInteractions(cliHttpClient); + } + +} diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/CliCommandsTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/CliCommandsTests.java new file mode 100644 index 00000000000..13aee4a4291 --- /dev/null +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/CliCommandsTests.java @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.cli.CliHttpClient; +import org.elasticsearch.xpack.sql.cli.TestTerminal; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class CliCommandsTests extends ESTestCase { + + public void testCliCommands() { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient cliHttpClient = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(cliHttpClient); + CliCommands cliCommands = new CliCommands( + (terminal, session, line) -> line.equals("foo"), + (terminal, session, line) -> line.equals("bar"), + (terminal, session, line) -> line.equals("baz") + ); + + assertTrue(cliCommands.handle(testTerminal, cliSession, "foo")); + assertTrue(cliCommands.handle(testTerminal, cliSession, "bar")); + assertTrue(cliCommands.handle(testTerminal, cliSession, "baz")); + assertFalse(cliCommands.handle(testTerminal, cliSession, "")); + assertFalse(cliCommands.handle(testTerminal, cliSession, "something")); + verifyNoMoreInteractions(cliHttpClient); + } +} diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/ServerInfoCliCommandTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/ServerInfoCliCommandTests.java new file mode 100644 index 00000000000..18f281e0368 --- /dev/null +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/ServerInfoCliCommandTests.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.cli.CliHttpClient; +import org.elasticsearch.xpack.sql.cli.TestTerminal; +import org.elasticsearch.xpack.sql.cli.net.protocol.InfoResponse; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class ServerInfoCliCommandTests extends ESTestCase { + + public void testInvalidCommand() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient client = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(client); + ServerInfoCliCommand cliCommand = new ServerInfoCliCommand(); + assertFalse(cliCommand.handle(testTerminal, cliSession, "blah")); + assertEquals(testTerminal.toString(), ""); + verifyNoMoreInteractions(client); + } + + public void testShowInfo() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient client = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(client); + when(client.serverInfo()).thenReturn(new InfoResponse("my_node", "my_cluster", (byte) 1, (byte) 2, "v1.2", "1234", "Sep 1, 2017")); + ServerInfoCliCommand cliCommand = new ServerInfoCliCommand(); + assertTrue(cliCommand.handle(testTerminal, cliSession, "info")); + assertEquals(testTerminal.toString(), "Node:my_node Cluster:my_cluster Version:v1.2\n"); + verify(client, times(1)).serverInfo(); + verifyNoMoreInteractions(client); + } + +} \ No newline at end of file diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/ServerQueryCliCommandTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/ServerQueryCliCommandTests.java new file mode 100644 index 00000000000..02c9faa778e --- /dev/null +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/command/ServerQueryCliCommandTests.java @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.cli.command; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.cli.CliHttpClient; +import org.elasticsearch.xpack.sql.cli.TestTerminal; +import org.elasticsearch.xpack.sql.cli.net.protocol.QueryInitResponse; +import org.elasticsearch.xpack.sql.cli.net.protocol.QueryPageResponse; + +import java.sql.SQLException; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class ServerQueryCliCommandTests extends ESTestCase { + + public void testExceptionHandling() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient client = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(client); + when(client.queryInit("blah", 1000)).thenThrow(new SQLException("test exception")); + ServerQueryCliCommand cliCommand = new ServerQueryCliCommand(); + assertTrue(cliCommand.handle(testTerminal, cliSession, "blah")); + assertEquals("Bad request [test exception]\n", testTerminal.toString()); + verify(client, times(1)).queryInit(eq("blah"), eq(1000)); + verifyNoMoreInteractions(client); + } + + public void testOnePageQuery() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient client = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(client); + cliSession.setFetchSize(10); + when(client.queryInit("test query", 10)).thenReturn(new QueryInitResponse(123, "", "some command response")); + ServerQueryCliCommand cliCommand = new ServerQueryCliCommand(); + assertTrue(cliCommand.handle(testTerminal, cliSession, "test query")); + assertEquals("some command response", testTerminal.toString()); + verify(client, times(1)).queryInit(eq("test query"), eq(10)); + verifyNoMoreInteractions(client); + } + + public void testThreePageQuery() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient client = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(client); + cliSession.setFetchSize(10); + when(client.queryInit("test query", 10)).thenReturn(new QueryInitResponse(123, "my_cursor1", "first")); + when(client.nextPage("my_cursor1")).thenReturn(new QueryPageResponse(345, "my_cursor2", "second")); + when(client.nextPage("my_cursor2")).thenReturn(new QueryPageResponse(678, "", "third")); + ServerQueryCliCommand cliCommand = new ServerQueryCliCommand(); + assertTrue(cliCommand.handle(testTerminal, cliSession, "test query")); + assertEquals("firstsecondthird", testTerminal.toString()); + verify(client, times(1)).queryInit(eq("test query"), eq(10)); + verify(client, times(2)).nextPage(any()); + verifyNoMoreInteractions(client); + } + + public void testTwoPageQueryWithSeparator() throws Exception { + TestTerminal testTerminal = new TestTerminal(); + CliHttpClient client = mock(CliHttpClient.class); + CliSession cliSession = new CliSession(client); + cliSession.setFetchSize(15); + // Set a separator + cliSession.setFetchSeparator("-----"); + when(client.queryInit("test query", 15)).thenReturn(new QueryInitResponse(123, "my_cursor1", "first")); + when(client.nextPage("my_cursor1")).thenReturn(new QueryPageResponse(345, "", "second")); + ServerQueryCliCommand cliCommand = new ServerQueryCliCommand(); + assertTrue(cliCommand.handle(testTerminal, cliSession, "test query")); + assertEquals("first-----\nsecond", testTerminal.toString()); + verify(client, times(1)).queryInit(eq("test query"), eq(15)); + verify(client, times(1)).nextPage(any()); + verifyNoMoreInteractions(client); + } + +} \ No newline at end of file