SQL: Switch CLI to core-cli's Command and make it testable (elastic/x-pack-elasticsearch#3232)

Switches CLI to use the standard Elasticsearch Command and refactors it to be more testable. It doesn't change any cli functionality except using the bright color while displaying query results.

relates elastic/x-pack-elasticsearch#2881, elastic/x-pack-elasticsearch#3203, elastic/x-pack-elasticsearch#2990

Original commit: elastic/x-pack-elasticsearch@841f306d50
This commit is contained in:
Igor Motov 2017-12-06 11:27:51 -05:00 committed by GitHub
parent 9664363575
commit 6839f99ed0
35 changed files with 1490 additions and 501 deletions

View File

@ -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());
}
}

View File

@ -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<String, String> 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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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 {

View File

@ -0,0 +1 @@
98cafc6081d5632b61be2c9e60650b64ddbc637c

View File

@ -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.
*/

View File

View File

@ -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<Boolean> debugOption;
private final OptionSpec<String> 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<String> 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);
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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<CliCommand> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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", "");
}
}

View File

@ -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<String> 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("<em>").append(text).append("</em>");
return this;
}
@Override
public LineBuilder error(String text) {
stringBuilder.append("<b>").append(text).append("</b>");
return this;
}
@Override
public LineBuilder param(String text) {
stringBuilder.append("<i>").append(text).append("</i>");
return this;
}
@Override
public void ln() {
stringBuilder.append("\n");
}
@Override
public void end() {
stringBuilder.append("<flush/>");
}
};
}
@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("<b>").append(type).append(" [</b>");
stringBuilder.append("<i>").append(message).append("</i>");
stringBuilder.append("<b>]</b>\n");
}
@Override
public void println() {
stringBuilder.append("\n");
}
@Override
public void clear() {
stringBuilder = new StringBuilder();
}
@Override
public void flush() {
stringBuilder.append("<flush/>");
}
@Override
public void printStackTrace(Exception ex) {
stringBuilder.append("<stack/>");
}
@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();
}
}

View File

@ -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 \"<em>foo</em>\"<flush/>", testTerminal.toString());
testTerminal.clear();
assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_separator=\"bar\""));
assertEquals("bar", cliSession.getFetchSeparator());
assertEquals("fetch separator set to \"<em>bar</em>\"<flush/>", testTerminal.toString());
testTerminal.clear();
assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch separator=\"baz\""));
assertEquals("baz", cliSession.getFetchSeparator());
assertEquals("fetch separator set to \"<em>baz</em>\"<flush/>", 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("<b>Invalid fetch size [</b><i>\"foo\"</i><b>]</b><flush/>", testTerminal.toString());
testTerminal.clear();
assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_size = 10"));
assertEquals(10L, cliSession.getFetchSize());
assertEquals("fetch size set to <em>10</em><flush/>", testTerminal.toString());
testTerminal.clear();
assertTrue(cliCommand.handle(testTerminal, cliSession, "fetch_size = -10"));
assertEquals(10L, cliSession.getFetchSize());
assertEquals("<b>Invalid fetch size [</b><i>-10</i><b>]. Must be > 0.</b><flush/>", 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);
}
}

View File

@ -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);
}
}

View File

@ -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:<em>my_node</em> Cluster:<em>my_cluster</em> Version:<em>v1.2</em>\n");
verify(client, times(1)).serverInfo();
verifyNoMoreInteractions(client);
}
}

View File

@ -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("<b>Bad request [</b><i>test exception</i><b>]</b>\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<flush/>", 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<flush/>", 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<flush/>", testTerminal.toString());
verify(client, times(1)).queryInit(eq("test query"), eq(15));
verify(client, times(1)).nextPage(any());
verifyNoMoreInteractions(client);
}
}