diff --git a/src/main/java/sqlline/AbstractCommandHandler.java b/src/main/java/sqlline/AbstractCommandHandler.java index 0fc88307..68125331 100644 --- a/src/main/java/sqlline/AbstractCommandHandler.java +++ b/src/main/java/sqlline/AbstractCommandHandler.java @@ -39,9 +39,7 @@ public AbstractCommandHandler(SqlLine sqlLine, String[] names, this.parameterCompleters = Collections.singletonList(new NullCompleter()); } else { - List c = new ArrayList<>(completers); - c.add(new NullCompleter()); - this.parameterCompleters = c; + this.parameterCompleters = new ArrayList<>(completers); } } diff --git a/src/main/java/sqlline/Application.java b/src/main/java/sqlline/Application.java index 626ff211..2f4d7784 100644 --- a/src/main/java/sqlline/Application.java +++ b/src/main/java/sqlline/Application.java @@ -89,6 +89,17 @@ public class Application { private static final List DEFAULT_CONNECTION_URL_EXAMPLES = Collections.unmodifiableList(Arrays.asList(CONNECTION_URLS)); + private static final String[] ISOLATION_LEVELS = { + "TRANSACTION_NONE", + "TRANSACTION_READ_COMMITTED", + "TRANSACTION_READ_UNCOMMITTED", + "TRANSACTION_REPEATABLE_READ", + "TRANSACTION_SERIALIZABLE" + }; + + private static final List ISOLATION_LEVEL_LIST = + Collections.unmodifiableList(Arrays.asList(ISOLATION_LEVELS)); + /** Creates an Application. */ public Application() { } @@ -211,6 +222,14 @@ public Collection getCommandHandlers(SqlLine sqlLine) { TableNameCompleter tableCompleter = new TableNameCompleter(sqlLine); List empty = Collections.emptyList(); final Map outputFormats = getOutputFormats(sqlLine); + final Map> propertyValues = + new HashMap>() {{ + put(BuiltInProperty.OUTPUT_FORMAT, outputFormats.keySet()); + }}; + final Map> customCompletions = + new HashMap<>(); + customCompletions + .put(BuiltInProperty.OUTPUT_FORMAT, outputFormats.keySet()); final CommandHandler[] handlers = { new ReflectiveCommandHandler(sqlLine, empty, "quit", "done", "exit"), new ReflectiveCommandHandler(sqlLine, @@ -264,9 +283,9 @@ public Collection getCommandHandlers(SqlLine sqlLine) { new ReflectiveCommandHandler(sqlLine, empty, "rollback"), new ReflectiveCommandHandler(sqlLine, empty, "help", "?"), new ReflectiveCommandHandler(sqlLine, - getOpts(sqlLine).optionCompleters(), "set"), + getOpts(sqlLine).setOptionCompleters(customCompletions), "set"), new ReflectiveCommandHandler(sqlLine, - getOpts(sqlLine).optionCompleters(), "reset"), + getOpts(sqlLine).resetOptionCompleters(), "reset"), new ReflectiveCommandHandler(sqlLine, empty, "save"), new ReflectiveCommandHandler(sqlLine, empty, "scan"), new ReflectiveCommandHandler(sqlLine, empty, "sql"), @@ -307,13 +326,8 @@ private Set getMetadataMethodNames() { } } - private List getIsolationLevels() { - return Arrays.asList( - "TRANSACTION_NONE", - "TRANSACTION_READ_COMMITTED", - "TRANSACTION_READ_UNCOMMITTED", - "TRANSACTION_REPEATABLE_READ", - "TRANSACTION_SERIALIZABLE"); + List getIsolationLevels() { + return ISOLATION_LEVEL_LIST; } public Map getName2HighlightStyle() { diff --git a/src/main/java/sqlline/BuiltInProperty.java b/src/main/java/sqlline/BuiltInProperty.java index 680a1a09..a6ca9a10 100644 --- a/src/main/java/sqlline/BuiltInProperty.java +++ b/src/main/java/sqlline/BuiltInProperty.java @@ -12,6 +12,9 @@ package sqlline; import java.io.File; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import org.jline.reader.LineReader; import org.jline.reader.impl.history.DefaultHistory; @@ -26,7 +29,8 @@ public enum BuiltInProperty implements SqlLineProperty { AUTO_COMMIT("autoCommit", Type.BOOLEAN, true), AUTO_SAVE("autoSave", Type.BOOLEAN, false), - COLOR_SCHEME("colorScheme", Type.STRING, DEFAULT), + COLOR_SCHEME("colorScheme", Type.STRING, DEFAULT, true, false, + new Application().getName2HighlightStyle().keySet()), COLOR("color", Type.BOOLEAN, false), CSV_DELIMITER("csvDelimiter", Type.STRING, ","), @@ -41,12 +45,13 @@ public enum BuiltInProperty implements SqlLineProperty { HISTORY_FILE("historyFile", Type.STRING, new File(SqlLineOpts.saveDir(), "history").getAbsolutePath()), INCREMENTAL("incremental", Type.BOOLEAN, false), - ISOLATION("isolation", Type.STRING, "TRANSACTION_REPEATABLE_READ"), + ISOLATION("isolation", Type.STRING, "TRANSACTION_REPEATABLE_READ", + true, false, new HashSet<>(new Application().getIsolationLevels())), MAX_COLUMN_WIDTH("maxColumnWidth", Type.INTEGER, -1), // don't save maxheight, maxwidth: it is automatically set based on // the terminal configuration - MAX_HEIGHT("maxHeight", Type.INTEGER, 80, false, false), - MAX_WIDTH("maxWidth", Type.INTEGER, 80, false, false), + MAX_HEIGHT("maxHeight", Type.INTEGER, 80, false, false, null), + MAX_WIDTH("maxWidth", Type.INTEGER, 80, false, false, null), MAX_HISTORY_ROWS("maxHistoryRows", Type.INTEGER, DefaultHistory.DEFAULT_HISTORY_SIZE), @@ -72,16 +77,18 @@ public enum BuiltInProperty implements SqlLineProperty { TRIM_SCRIPTS("trimScripts", Type.BOOLEAN, true), USE_LINE_CONTINUATION("useLineContinuation", Type.BOOLEAN, true), VERBOSE("verbose", Type.BOOLEAN, false), - VERSION("version", Type.STRING, new Application().getVersion(), false, true); + VERSION("version", Type.STRING, new Application().getVersion(), false, true, + null); private final String propertyName; private final Type type; private final Object defaultValue; private final boolean couldBeStored; private final boolean isReadOnly; + private final Set availableValues; BuiltInProperty(String propertyName, Type type, Object defaultValue) { - this(propertyName, type, defaultValue, true, false); + this(propertyName, type, defaultValue, true, false, null); } BuiltInProperty( @@ -89,12 +96,15 @@ public enum BuiltInProperty implements SqlLineProperty { Type type, Object defaultValue, boolean couldBeStored, - boolean isReadOnly) { + boolean isReadOnly, + Set availableValues) { this.propertyName = propertyName; this.type = type; this.defaultValue = defaultValue; this.isReadOnly = isReadOnly; this.couldBeStored = couldBeStored; + this.availableValues = availableValues == null + ? Collections.emptySet() : Collections.unmodifiableSet(availableValues); } @Override public String propertyName() { @@ -117,6 +127,10 @@ public enum BuiltInProperty implements SqlLineProperty { return type; } + @Override public Set getAvailableValues() { + return availableValues; + } + /** Returns the built-in property with the given name, or null if not found. * * @param propertyName Property name diff --git a/src/main/java/sqlline/Commands.java b/src/main/java/sqlline/Commands.java index eb5b0048..8ad279c8 100644 --- a/src/main/java/sqlline/Commands.java +++ b/src/main/java/sqlline/Commands.java @@ -18,11 +18,14 @@ import java.nio.charset.StandardCharsets; import java.sql.*; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.jline.reader.EOFError; import org.jline.reader.History; import org.jline.reader.Parser; import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; /** * Collection of available commands. @@ -480,6 +483,21 @@ public void dropall(String line, DispatchCallback callback) { } } + private int getUserAnswer( + String question, int... allowedAnswers) throws IOException { + final Set allowedAnswerSet = + IntStream.of(allowedAnswers).boxed().collect(Collectors.toSet()); + final Terminal terminal = sqlLine.getLineReader().getTerminal(); + final PrintWriter writer = terminal.writer(); + writer.write(question); + int c; + // The logic to prevent reaction of SqlLineParser here + do { + c = terminal.reader().read(100); + } while (c != -1 && !allowedAnswerSet.contains(c)); + return c; + } + public void reconnect(String line, DispatchCallback callback) { DatabaseConnection databaseConnection = sqlLine.getDatabaseConnection(); if (databaseConnection == null || databaseConnection.getUrl() == null) { @@ -1714,14 +1732,8 @@ private void sillyLess(InputStream in) throws IOException { // silly little pager if (index % (sqlLine.getOpts().getMaxHeight() - 1) == 0) { - String prompt = sqlLine.loc("enter-for-more"); - sqlLine.getLineReader().getTerminal().writer().write(prompt); - int c; - // The logic to prevent reaction of SqlLineParser here - do { - c = sqlLine.getLineReader().getTerminal().reader().read(100); - } while (c != -1 && c != 13 && c != 'q'); - if (c == -1 || c == 'q') { + int userInput = getUserAnswer(sqlLine.loc("enter-for-more"), 'q', 13); + if (userInput == -1 || userInput == 'q') { sqlLine.getLineReader().getTerminal().writer().write('\n'); break; } diff --git a/src/main/java/sqlline/SqlLineCommandCompleter.java b/src/main/java/sqlline/SqlLineCommandCompleter.java index c88a431a..ba367136 100644 --- a/src/main/java/sqlline/SqlLineCommandCompleter.java +++ b/src/main/java/sqlline/SqlLineCommandCompleter.java @@ -14,6 +14,7 @@ import java.util.LinkedList; import java.util.List; +import org.jline.builtins.Completers; import org.jline.reader.Completer; import org.jline.reader.impl.completer.AggregateCompleter; import org.jline.reader.impl.completer.ArgumentCompleter; @@ -26,16 +27,23 @@ class SqlLineCommandCompleter extends AggregateCompleter { SqlLineCommandCompleter(SqlLine sqlLine) { super(new LinkedList<>()); - List completers = new LinkedList<>(); + List completers = new LinkedList<>(); for (CommandHandler commandHandler : sqlLine.getCommandHandlers()) { for (String cmd : commandHandler.getNames()) { List compl = new LinkedList<>(); - compl.add(new StringsCompleter(SqlLine.COMMAND_PREFIX + cmd)); - compl.addAll(commandHandler.getParameterCompleters()); - compl.add(new NullCompleter()); // last param no complete - - completers.add(new ArgumentCompleter(compl)); + final List parameterCompleters = + commandHandler.getParameterCompleters(); + if (parameterCompleters.size() == 1 + && parameterCompleters.iterator().next() + instanceof Completers.RegexCompleter) { + completers.add(parameterCompleters.iterator().next()); + } else { + compl.add(new StringsCompleter(SqlLine.COMMAND_PREFIX + cmd)); + compl.addAll(parameterCompleters); + compl.add(new NullCompleter()); // last param no complete + completers.add(new ArgumentCompleter(compl)); + } } } diff --git a/src/main/java/sqlline/SqlLineOpts.java b/src/main/java/sqlline/SqlLineOpts.java index adb1b1ba..8edcaa2b 100644 --- a/src/main/java/sqlline/SqlLineOpts.java +++ b/src/main/java/sqlline/SqlLineOpts.java @@ -19,6 +19,7 @@ import java.util.*; import java.util.stream.Collectors; +import org.jline.builtins.Completers; import org.jline.keymap.KeyMap; import org.jline.reader.Binding; import org.jline.reader.Candidate; @@ -116,10 +117,84 @@ public SqlLineOpts(SqlLine sqlLine, Properties props) { loadProperties(props); } - public List optionCompleters() { + public List resetOptionCompleters() { return Collections.singletonList(this); } + /** + * Builds and returns {@link org.jline.builtins.Completers.RegexCompleter} for + * !set command based on + * (in decreasing order of priority) + *
    + *
  • Customizations via {@code customCompletions}
  • + *
  • Available values defined in {@link BuiltInProperty}
  • + *
  • {@link Type} of property. + * Currently there is completion only for boolean type
  • + *
+ * + * @param customCompletions defines custom completions values per property + * @return a singleton list with a built RegexCompleter */ + public List setOptionCompleters( + Map> customCompletions) { + Map comp = new HashMap<>(); + final String start = "START"; + comp.put(start, new StringsCompleter("!set")); + Collection booleanProperties = new ArrayList<>(); + Collection withDefinedAvailableValues = new ArrayList<>(); + StringBuilder sb = new StringBuilder(start + " ("); + for (BuiltInProperty property : BuiltInProperty.values()) { + if (customCompletions.containsKey(property)) { + continue; + } else if (!property.getAvailableValues().isEmpty()) { + withDefinedAvailableValues.add(property); + } else if (property.type() == Type.BOOLEAN) { + booleanProperties.add(property); + } else { + sb.append(property.name()).append(" | "); + comp.put(property.name(), + new StringsCompleter(property.propertyName())); + } + } + // all boolean properties without defined available values and + // not customized via {@code customCompletions} have values + // for autocompletion specified in SqlLineProperty.BOOLEAN_VALUES + final String booleanTypeString = Type.BOOLEAN.toString(); + sb.append(booleanTypeString); + comp.put(booleanTypeString, + new StringsCompleter(booleanProperties + .stream() + .map(BuiltInProperty::propertyName) + .toArray(String[]::new))); + final String booleanPropertyValueKey = booleanTypeString + "_value"; + comp.put(booleanPropertyValueKey, + new StringsCompleter(BuiltInProperty.BOOLEAN_VALUES)); + sb.append(" ").append(booleanPropertyValueKey); + // If a property has defined values they will be used for autocompletion + for (BuiltInProperty property : withDefinedAvailableValues) { + final String propertyName = property.propertyName(); + sb.append(" | ").append(propertyName); + comp.put(propertyName, new StringsCompleter(propertyName)); + final String propertyValueKey = propertyName + "_value"; + comp.put(propertyValueKey, + new StringsCompleter( + property.getAvailableValues().toArray(new String[0]))); + sb.append(" ").append(propertyValueKey); + } + for (Map.Entry> mapEntry + : customCompletions.entrySet()) { + final String propertyName = mapEntry.getKey().propertyName(); + comp.put(propertyName, new StringsCompleter(propertyName)); + final String propertyValueKey = propertyName + "_value"; + comp.put(propertyValueKey, + new StringsCompleter(mapEntry.getValue().toArray(new String[0]))); + sb.append("| ").append(propertyName).append(" ") + .append(propertyValueKey); + } + sb.append(") "); + return Collections.singletonList( + new Completers.RegexCompleter(sb.toString(), comp::get)); + } + /** * The save directory if HOME/.sqlline/ on UNIX, and HOME/sqlline/ on * Windows. diff --git a/src/main/java/sqlline/SqlLineProperty.java b/src/main/java/sqlline/SqlLineProperty.java index a06019e2..70436dfa 100644 --- a/src/main/java/sqlline/SqlLineProperty.java +++ b/src/main/java/sqlline/SqlLineProperty.java @@ -11,6 +11,8 @@ */ package sqlline; +import java.util.Set; + /** * Definition of property that may be specified for SqlLine. * @@ -18,6 +20,9 @@ */ public interface SqlLineProperty { String DEFAULT = "default"; + String[] BOOLEAN_VALUES = { + Boolean.TRUE.toString(), Boolean.FALSE.toString()}; + String propertyName(); Object defaultValue(); @@ -28,6 +33,8 @@ public interface SqlLineProperty { Type type(); + Set getAvailableValues(); + /** Property writer. */ @FunctionalInterface interface Writer { @@ -38,8 +45,8 @@ interface Writer { enum Type { BOOLEAN, CHAR, - STRING, - INTEGER; + INTEGER, + STRING; } } diff --git a/src/test/java/sqlline/CompletionTest.java b/src/test/java/sqlline/CompletionTest.java new file mode 100644 index 00000000..e24a5643 --- /dev/null +++ b/src/test/java/sqlline/CompletionTest.java @@ -0,0 +1,249 @@ +/* +// Licensed to Julian Hyde under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. +// +// Julian Hyde licenses this file to you under the Modified BSD License +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/BSD-3-Clause +*/ +package sqlline; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.impl.LineReaderImpl; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Test cases for Completions. + */ +public class CompletionTest { + private final SqlLine sqlLine = new SqlLine(); + + @Test + public void testCommandCompletions() { + final LineReaderCompletionImpl lineReader = getDummyLineReader(); + + TreeSet commandSet = getCommandSet(sqlLine); + + // check completions of ! + one symbol + for (char c = 'a'; c <= 'z'; c++) { + final Set expectedSubSet = filterSet(commandSet, "!" + c); + final Set actual = getLineReaderCompletedSet(lineReader, "!" + c); + assertEquals("Completion for command !" + c, expectedSubSet, actual); + } + + // check completions if the whole command is finished + final Set quitExpected = filterSet(commandSet, "!quit"); + final Set quitActual = + getLineReaderCompletedSet(lineReader, "!quit"); + assertEquals(quitExpected, quitActual); + } + + @Test + public void testResetPropertyCompletions() { + final LineReaderCompletionImpl lineReader = getDummyLineReader(); + final String resetCommand = "!reset "; + + for (BuiltInProperty property: BuiltInProperty.values()) { + final String command = resetCommand + + property.propertyName().toLowerCase(Locale.ROOT); + final Set actual = + getLineReaderCompletedSet(lineReader, command + " "); + assertEquals("Completion for command '" + + resetCommand + property.propertyName() + "'", + Collections.singleton(command), actual); + } + } + + @Test + public void testSetPropertyCompletions() { + final LineReaderCompletionImpl lineReader = getDummyLineReader(); + final Set actual = + getLineReaderCompletedSet(lineReader, "!set verbose tr"); + assertEquals("!set verbose true", actual.iterator().next()); + + final Set jsonActual = + getLineReaderCompletedSet(lineReader, "!set outputFormat js"); + assertEquals(1, jsonActual.size()); + assertEquals("!set outputFormat json", jsonActual.iterator().next()); + + final Set xmlelActual = + getLineReaderCompletedSet(lineReader, "!set outputFormat xmlel"); + assertEquals(1, xmlelActual.size()); + assertEquals("!set outputFormat xmlelements", + xmlelActual.iterator().next()); + + final Set chesterActual = + getLineReaderCompletedSet(lineReader, "!set colorScheme che"); + assertEquals(1, chesterActual.size()); + assertEquals("!set colorScheme chester", chesterActual.iterator().next()); + + final Set solarizedActual = + getLineReaderCompletedSet(lineReader, "!set colorScheme sol"); + assertEquals(1, solarizedActual.size()); + assertEquals("!set colorScheme solarized", + solarizedActual.iterator().next()); + } + + @Test + public void testSqlCompletions() { + try { + SqlLine sqlLine = new SqlLine(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + SqlLine.Status status = begin(sqlLine, os, false); + // Here the status is SqlLine.Status.OTHER + // because of EOF as the result of InputStream which + // is not used in the current test so it is ok + assertThat(status, equalTo(SqlLine.Status.OTHER)); + sqlLine.runCommands(new DispatchCallback(), + "!set maxwidth 80", + "!connect " + + SqlLineArgsTest.ConnectionSpec.H2.url + " " + + SqlLineArgsTest.ConnectionSpec.H2.username + " " + + "\"\""); + os.reset(); + + LineReader lineReader = sqlLine.getLineReader(); + + LineReaderCompletionImpl lineReaderCompletion = + new LineReaderCompletionImpl(lineReader.getTerminal()); + lineReaderCompletion.setCompleter(new SqlLineCompleter(sqlLine)); + final Set lowCaseActual = + getLineReaderCompletedSet(lineReaderCompletion, "sel"); + assertEquals(1, lowCaseActual.size()); + assertEquals("select", lowCaseActual.iterator().next()); + + final Set upperCaseActual = + getLineReaderCompletedSet(lineReaderCompletion, "SEL"); + assertEquals(1, upperCaseActual.size()); + assertEquals("SELECT", upperCaseActual.iterator().next()); + } catch (Exception e) { + // fail + throw new RuntimeException(e); + } + } + + private LineReaderCompletionImpl getDummyLineReader() { + try { + TerminalBuilder terminalBuilder = TerminalBuilder.builder(); + final Terminal terminal = terminalBuilder.build(); + + final LineReaderCompletionImpl lineReader = + new LineReaderCompletionImpl(terminal); + lineReader.setCompleter(sqlLine.getCommandCompleter()); + lineReader.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true); + return lineReader; + } catch (Exception e) { + // fail + throw new RuntimeException(e); + } + } + + private TreeSet getCommandSet(SqlLine sqlLine) { + TreeSet commandSet = new TreeSet<>(); + for (CommandHandler ch: sqlLine.getCommandHandlers()) { + for (String name: ch.getNames()) { + commandSet.add("!" + name); + } + commandSet.add("!" + ch.getName()); + } + return commandSet; + } + + private Set getLineReaderCompletedSet( + LineReaderCompletionImpl lineReader, String s) { + return lineReader.complete(s).stream() + .map(Candidate::value).collect(Collectors.toCollection(TreeSet::new)); + } + + private Set filterSet(TreeSet commandSet, String s2) { + Set subset = commandSet.stream() + .filter(s -> s.startsWith(s2)) + .collect(Collectors.toCollection(TreeSet::new)); + if (subset.isEmpty()) { + Set result = new TreeSet<>(commandSet); + result.add(s2); + return result; + } else { + return subset; + } + } + + /** LineReaderImpl extension to test completion*/ + private class LineReaderCompletionImpl extends LineReaderImpl { + List list; + + LineReaderCompletionImpl(Terminal terminal) throws IOException { + super(terminal); + } + + List complete(final String line) { + buf.write(line); + buf.cursor(line.length()); + doComplete(CompletionType.Complete, true, true); + final String buffer = buf.toString(); + buf.clear(); + return list == null || buffer.length() > line.length() + ? Collections.singletonList(new Candidate(buffer.trim())) + : list; + } + + protected boolean doMenu( + List original, + String completed, + BiFunction escaper) { + list = original; + return true; + } + } + + private static SqlLine.Status begin( + SqlLine sqlLine, OutputStream os, boolean saveHistory, String... args) { + try { + PrintStream beelineOutputStream = getPrintStream(os); + sqlLine.setOutputStream(beelineOutputStream); + sqlLine.setErrorStream(beelineOutputStream); + final InputStream is = new ByteArrayInputStream(new byte[0]); + return sqlLine.begin(args, is, saveHistory); + } catch (Throwable t) { + // fail + throw new RuntimeException(t); + } + } + + private static PrintStream getPrintStream(OutputStream os) { + try { + return new PrintStream(os, false, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + // fail + throw new RuntimeException(e); + } + } +} + +// End CompletionTest.java