/*
// 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.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
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.History;
import org.jline.reader.MaskingCallback;
import org.jline.reader.Parser;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.LineReaderImpl;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;

import static sqlline.SqlLine.rpad;

/**
 * Collection of available commands.
 */
public class Commands {
  private static final String[] METHODS = {
      "allProceduresAreCallable",
      "allTablesAreSelectable",
      "dataDefinitionCausesTransactionCommit",
      "dataDefinitionIgnoredInTransactions",
      "doesMaxRowSizeIncludeBlobs",
      "getCatalogSeparator",
      "getCatalogTerm",
      "getDatabaseProductName",
      "getDatabaseProductVersion",
      "getDefaultTransactionIsolation",
      "getDriverMajorVersion",
      "getDriverMinorVersion",
      "getDriverName",
      "getDriverVersion",
      "getExtraNameCharacters",
      "getIdentifierQuoteString",
      "getMaxBinaryLiteralLength",
      "getMaxCatalogNameLength",
      "getMaxCharLiteralLength",
      "getMaxColumnNameLength",
      "getMaxColumnsInGroupBy",
      "getMaxColumnsInIndex",
      "getMaxColumnsInOrderBy",
      "getMaxColumnsInSelect",
      "getMaxColumnsInTable",
      "getMaxConnections",
      "getMaxCursorNameLength",
      "getMaxIndexLength",
      "getMaxProcedureNameLength",
      "getMaxRowSize",
      "getMaxSchemaNameLength",
      "getMaxStatementLength",
      "getMaxStatements",
      "getMaxTableNameLength",
      "getMaxTablesInSelect",
      "getMaxUserNameLength",
      "getNumericFunctions",
      "getProcedureTerm",
      "getSchemaTerm",
      "getSearchStringEscape",
      "getSQLKeywords",
      "getStringFunctions",
      "getSystemFunctions",
      "getTimeDateFunctions",
      "getURL",
      "getUserName",
      "isCatalogAtStart",
      "isReadOnly",
      "nullPlusNonNullIsNull",
      "nullsAreSortedAtEnd",
      "nullsAreSortedAtStart",
      "nullsAreSortedHigh",
      "nullsAreSortedLow",
      "storesLowerCaseIdentifiers",
      "storesLowerCaseQuotedIdentifiers",
      "storesMixedCaseIdentifiers",
      "storesMixedCaseQuotedIdentifiers",
      "storesUpperCaseIdentifiers",
      "storesUpperCaseQuotedIdentifiers",
      "supportsAlterTableWithAddColumn",
      "supportsAlterTableWithDropColumn",
      "supportsANSI92EntryLevelSQL",
      "supportsANSI92FullSQL",
      "supportsANSI92IntermediateSQL",
      "supportsBatchUpdates",
      "supportsCatalogsInDataManipulation",
      "supportsCatalogsInIndexDefinitions",
      "supportsCatalogsInPrivilegeDefinitions",
      "supportsCatalogsInProcedureCalls",
      "supportsCatalogsInTableDefinitions",
      "supportsColumnAliasing",
      "supportsConvert",
      "supportsCoreSQLGrammar",
      "supportsCorrelatedSubqueries",
      "supportsDataDefinitionAndDataManipulationTransactions",
      "supportsDataManipulationTransactionsOnly",
      "supportsDifferentTableCorrelationNames",
      "supportsExpressionsInOrderBy",
      "supportsExtendedSQLGrammar",
      "supportsFullOuterJoins",
      "supportsGroupBy",
      "supportsGroupByBeyondSelect",
      "supportsGroupByUnrelated",
      "supportsIntegrityEnhancementFacility",
      "supportsLikeEscapeClause",
      "supportsLimitedOuterJoins",
      "supportsMinimumSQLGrammar",
      "supportsMixedCaseIdentifiers",
      "supportsMixedCaseQuotedIdentifiers",
      "supportsMultipleResultSets",
      "supportsMultipleTransactions",
      "supportsNonNullableColumns",
      "supportsOpenCursorsAcrossCommit",
      "supportsOpenCursorsAcrossRollback",
      "supportsOpenStatementsAcrossCommit",
      "supportsOpenStatementsAcrossRollback",
      "supportsOrderByUnrelated",
      "supportsOuterJoins",
      "supportsPositionedDelete",
      "supportsPositionedUpdate",
      "supportsSchemasInDataManipulation",
      "supportsSchemasInIndexDefinitions",
      "supportsSchemasInPrivilegeDefinitions",
      "supportsSchemasInProcedureCalls",
      "supportsSchemasInTableDefinitions",
      "supportsSelectForUpdate",
      "supportsStoredProcedures",
      "supportsSubqueriesInComparisons",
      "supportsSubqueriesInExists",
      "supportsSubqueriesInIns",
      "supportsSubqueriesInQuantifieds",
      "supportsTableCorrelationNames",
      "supportsTransactions",
      "supportsUnion",
      "supportsUnionAll",
      "usesLocalFilePerTable",
      "usesLocalFiles",
  };

  private static final String CONNECT_PROPERTY = "#CONNECT_PROPERTY#.";
  private final SqlLine sqlLine;

  Commands(SqlLine sqlLine) {
    this.sqlLine = sqlLine;
  }

  public void metadata(String line, DispatchCallback callback) {
    sqlLine.debug(line);

    String[] parts = sqlLine.split(line);
    if (parts == null || parts.length == 0) {
      dbinfo("", callback);
      return;
    }

    if (parts.length == 1) {
      sqlLine.error("Usage: metadata <methodname> <params...>");
      callback.setToFailure();
      return;
    }

    List<Object> params = new LinkedList<>(Arrays.asList(parts));
    params.remove(0);
    params.remove(0);
    sqlLine.debug(params.toString());
    metadata(parts[1], params, callback);
  }

  public void metadata(
      String cmd, List<Object> argList, DispatchCallback callback) {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }

    try {
      Set<String> methodNames = new TreeSet<>();
      Set<String> methodNamesUpper = new TreeSet<>();
      Class currentClass = sqlLine.getConnection().getMetaData().getClass();
      Object res = null;
      do {
        for (Method method : currentClass.getDeclaredMethods()) {
          final int modifiers = method.getModifiers();
          if (!Modifier.isPublic(modifiers)
              || Modifier.isStatic(modifiers)) {
            continue;
          }
          methodNames.add(method.getName());
          methodNamesUpper.add(method.getName().toUpperCase(Locale.ROOT));
          if (methodNamesUpper.contains(cmd.toUpperCase(Locale.ROOT))) {
            try {
              res = sqlLine.getReflector().invoke(
                  sqlLine.getDatabaseMetaData(),
                  sqlLine.getDatabaseMetaData().getClass(), cmd, argList);
            } catch (Exception e) {
              sqlLine.handleException(e);
              callback.setToFailure();
              return;
            }
            if (res != null) {
              break;
            }
          }
        }
        currentClass = currentClass.getSuperclass();
      } while (res == null
          && DatabaseMetaData.class.isAssignableFrom(currentClass));

      if (!methodNamesUpper.contains(cmd.toUpperCase(Locale.ROOT))) {
        sqlLine.error(sqlLine.loc("no-such-method", cmd));
        sqlLine.error(sqlLine.loc("possible-methods"));
        for (String methodName : methodNames) {
          sqlLine.error("   " + methodName);
        }
        callback.setToFailure();
        return;
      }

      if (res instanceof ResultSet) {
        try (ResultSet rs = (ResultSet) res) {
          sqlLine.print(rs, callback);
        }
      } else if (res != null) {
        sqlLine.output(res.toString());
      }
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
    callback.setToSuccess();
  }

  public void history(String line, DispatchCallback callback) {
    try {
      String argsLine = line.substring("history".length());
      org.jline.builtins.Commands.history(
          sqlLine.getLineReader(),
          sqlLine.getOutputStream(),
          sqlLine.getErrorStream(),
          argsLine.isEmpty()
              ? new String[]{sqlLine.getOpts().getHistoryFlags()}
              : sqlLine.split(argsLine, " "));
    } catch (Exception e) {
      callback.setToFailure();
    }
    callback.setToSuccess();
  }

  public void rerun(String line, DispatchCallback callback) {
    String[] cmd = sqlLine.split(line);
    History history = sqlLine.getLineReader().getHistory();
    int size = history.size();
    if (cmd.length > 2 || (cmd.length == 2 && !cmd[1].matches("-?\\d+"))) {
      if (size == 0) {
        sqlLine.error("Usage: rerun <offset>, history should not be empty");
      } else {
        sqlLine.error("Usage: rerun <offset>, available range of offset is -"
            + (size - 1) + ".." + size);
      }
      callback.setToFailure();
      return;
    }

    int offset = cmd.length == 1 ? -1 : Integer.parseInt(cmd[1]);
    if (size < offset || size - 1 < -offset || offset == 0) {
      if (offset == 0) {
        sqlLine.error(
            "Usage: rerun <offset>, offset should be positive or negative");
      }
      if (size == 0) {
        sqlLine.error("Usage: rerun <offset>, history should not be empty");
      } else {
        sqlLine.error("Usage: rerun <offset>, available range of offset is -"
            + (size - 1) + ".." + size);
      }
      callback.setToFailure();
      return;
    }

    sqlLine.dispatch(calculateCommand(offset, new HashSet<>()), callback);
  }

  private String calculateCommand(int currentOffset, Set<Integer> offsets) {
    if (!offsets.add(currentOffset)) {
      throw new IllegalArgumentException(
          "Cycled rerun of commands from history " + offsets);
    }

    History history = sqlLine.getLineReader().getHistory();
    Iterator<History.Entry> iterator = currentOffset > 0
        ? history.iterator(currentOffset - 1)
        : history.reverseIterator(history.size() - 1 + currentOffset);
    String command = iterator.next().line();
    if (command.trim().startsWith("!/") || command.startsWith("!rerun")) {
      String[] cmd = sqlLine.split(command);
      if (cmd.length > 2 || (cmd.length == 2 && !cmd[1].matches("-?\\d+"))) {
        return command;
      }
      int offset = cmd.length == 1 ? -1 : Integer.parseInt(cmd[1]);
      if (history.size() < offset || history.size() - 1 < -offset) {
        return command;
      }
      return calculateCommand(offset, offsets);
    }
    return command;
  }

  String arg1(String line, String paramName) {
    return arg1(line, paramName, null);
  }

  String arg1(String line, String paramName, String def) {
    String[] ret = sqlLine.split(line);

    if (ret == null || ret.length != 2) {
      if (def != null) {
        return def;
      }
      throw new IllegalArgumentException(
          sqlLine.loc("arg-usage",
              ret == null || ret.length == 0 ? "" : ret[0], paramName));
    }
    return ret[1];
  }

  /**
   * Constructs a list of string parameters for a metadata call.
   *
   * <p>The number of items is equal to the number of arguments once
   * the command line (the {@code line} parameter) has been parsed,
   * typically three (catalog, schema, table name).
   *
   * <p>Parses the command line, and assumes that the the first word is
   * a compound identifier. If the compound identifier has fewer parts
   * than required, fills from the right.
   *
   * <p>The result is a mutable list of strings.
   *
   * @param line          Command line
   * @param paramName     Name of parameter being read from command line
   * @param defaultValues Default values for each component of parameter
   * @return Mutable list of strings
   */
  private List<Object> buildMetadataArgs(
      String line,
      String paramName,
      String[] defaultValues) {
    final List<Object> list = new ArrayList<>();
    final String[][] ret = sqlLine.splitCompound(line);
    String[] compound;
    if (ret == null || ret.length != 2) {
      if (defaultValues[defaultValues.length - 1] == null) {
        throw new IllegalArgumentException(
            sqlLine.loc("arg-usage",
                ret == null || ret.length == 0 ? "" : ret[0][0], paramName));
      }
      compound = new String[0];
    } else {
      compound = ret[1];
    }
    if (compound.length <= defaultValues.length) {
      list.addAll(
          Arrays.asList(defaultValues).subList(
              0, defaultValues.length - compound.length));
      list.addAll(Arrays.asList(compound));
    } else {
      list.addAll(
          Arrays.asList(compound).subList(0, defaultValues.length));
    }
    return list;
  }

  public void indexes(String line, DispatchCallback callback) throws Exception {
    String[] strings = {sqlLine.getConnection().getCatalog(), null, "%"};
    List<Object> args = buildMetadataArgs(line, "table name", strings);
    args.add(Boolean.FALSE);
    args.add(Boolean.TRUE);
    metadata("getIndexInfo", args, callback);
  }

  public void primarykeys(String line, DispatchCallback callback)
      throws Exception {
    String[] strings = {sqlLine.getConnection().getCatalog(), null, "%"};
    List<Object> args = buildMetadataArgs(line, "table name", strings);
    metadata("getPrimaryKeys", args, callback);
  }

  public void exportedkeys(String line, DispatchCallback callback)
      throws Exception {
    String[] strings = {sqlLine.getConnection().getCatalog(), null, "%"};
    List<Object> args = buildMetadataArgs(line, "table name", strings);
    metadata("getExportedKeys", args, callback);
  }

  public void importedkeys(String line, DispatchCallback callback)
      throws Exception {
    String[] strings = {sqlLine.getConnection().getCatalog(), null, "%"};
    List<Object> args = buildMetadataArgs(line, "table name", strings);
    metadata("getImportedKeys", args, callback);
  }

  public void procedures(String line, DispatchCallback callback)
      throws Exception {
    String[] strings = {sqlLine.getConnection().getCatalog(), null, "%"};
    List<Object> args =
        buildMetadataArgs(line, "procedure name pattern", strings);
    metadata("getProcedures", args, callback);
  }

  public void tables(String line, DispatchCallback callback)
      throws SQLException {
    String[] strings = {sqlLine.getConnection().getCatalog(), null, "%"};
    List<Object> args = buildMetadataArgs(line, "table name", strings);
    args.add(null);
    metadata("getTables", args, callback);
  }

  public void schemas(String line, DispatchCallback callback) {
    metadata("getSchemas", Collections.emptyList(), callback);
  }

  public void typeinfo(String line, DispatchCallback callback) {
    metadata("getTypeInfo", Collections.emptyList(), callback);
  }

  public void nativesql(String sql, DispatchCallback callback)
      throws Exception {
    if (sql.startsWith(SqlLine.COMMAND_PREFIX)) {
      sql = sql.substring(1);
    }

    if (sql.startsWith("native")) {
      sql = sql.substring("native".length() + 1);
    }

    String nat = sqlLine.getConnection().nativeSQL(sql);
    sqlLine.output(nat);
    callback.setToSuccess();
  }

  public void columns(String line, DispatchCallback callback)
      throws SQLException {
    String[] strings = {sqlLine.getConnection().getCatalog(), null, "%"};
    List<Object> args = buildMetadataArgs(line, "table name", strings);
    args.add("%");
    metadata("getColumns", args, callback);
  }

  public void dropall(String line, DispatchCallback callback) {
    String[] parts = sqlLine.split(line);
    if (parts.length > 2) {
      sqlLine.error("Usage: !dropall [schema_pattern]");
      callback.setToFailure();
      return;
    }
    DatabaseConnection databaseConnection = sqlLine.getDatabaseConnection();
    if (databaseConnection == null || databaseConnection.getUrl() == null) {
      sqlLine.error(sqlLine.loc("no-current-connection"));
      callback.setToFailure();
      return;
    }
    try {
      String question = sqlLine.loc("really-drop-all");
      final int userResponse =
          getUserAnswer(question, 'y', 'n', 'Y', 'N');
      if (userResponse != 'y' && userResponse != 'Y') {
        sqlLine.error("abort-drop-all");
        callback.setToFailure();
        return;
      }

      List<String> cmds = new LinkedList<>();
      final char openQuote = sqlLine.getDialect().getOpenQuote();
      final char closeQuote = sqlLine.getDialect().getOpenQuote();
      try (ResultSet rs =
               sqlLine.getTables(parts.length > 1 ? parts[1] : null)) {
        while (rs.next()) {
          cmds.add("DROP TABLE " + openQuote + rs.getString("TABLE_SCHEM")
              + closeQuote + "." + openQuote
              + rs.getString("TABLE_NAME")  + closeQuote + ";");
        }
      }

      // run as a batch
      if (sqlLine.runCommands(cmds, callback) == cmds.size()) {
        callback.setToSuccess();
      } else {
        callback.setToFailure();
      }
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  int getUserAnswer(String question, int... allowedAnswers)
      throws IOException {
    final Set<Integer> allowedAnswerSet =
        IntStream.of(allowedAnswers).boxed().collect(Collectors.toSet());
    final Terminal terminal = sqlLine.getLineReader().getTerminal();
    final PrintWriter writer = terminal.writer();
    writer.write(question);
    writer.write('\n');
    writer.flush();
    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) {
      sqlLine.error(sqlLine.loc("no-current-connection"));
      callback.setToFailure();
      return;
    }

    sqlLine.info(sqlLine.loc("reconnecting", databaseConnection.getUrl()));
    try {
      databaseConnection.reconnect();
    } catch (Exception e) {
      sqlLine.error(e);
      callback.setToFailure();
      return;
    }

    callback.setToSuccess();
  }

  public void scan(String line, DispatchCallback callback) {
    final Map<String, Driver> driverNames = new TreeMap<>();

    if (sqlLine.getDrivers() == null) {
      sqlLine.setDrivers(sqlLine.scanDrivers());
    }

    sqlLine.info(
        sqlLine.loc("drivers-found-count", sqlLine.getDrivers().size()));

    // unique the list

    for (Driver driver : sqlLine.getDrivers()) {
      driverNames.put(driver.getClass().getName(), driver);
    }

    final String header = rpad(sqlLine.loc("compliant"), 10)
        + rpad(sqlLine.loc("jdbc-version"), 8)
        + sqlLine.loc("driver-class");
    sqlLine.output(new AttributedString(header, AttributedStyle.BOLD));

    for (Map.Entry<String, Driver> driverEntry : driverNames.entrySet()) {
      final String name = driverEntry.getKey();
      try {
        final Class<?> klass = Class.forName(name);
        Driver driver = (Driver) klass.getConstructor().newInstance();
        final String msg = rpad(driver.jdbcCompliant() ? "yes" : "no", 10)
            + rpad(driver.getMajorVersion() + "." + driver.getMinorVersion(), 8)
            + name;
        if (driver.jdbcCompliant()) {
          sqlLine.output(msg);
        } else {
          sqlLine.output(new AttributedString(msg, AttributedStyles.RED));
        }
      } catch (Throwable t) {
        // error with driver
        sqlLine.output(new AttributedString(name, AttributedStyles.RED));
      }
    }

    callback.setToSuccess();
  }

  public void save(String line, DispatchCallback callback)
      throws IOException {
    sqlLine.info(
        sqlLine.loc("saving-options", sqlLine.getOpts().getPropertiesFile()));
    sqlLine.getOpts().save();
    callback.setToSuccess();
  }

  public void load(String line, DispatchCallback callback) throws IOException {
    sqlLine.getOpts().load();
    sqlLine.info(
        sqlLine.loc("loaded-options",
            sqlLine.getOpts().getPropertiesFile()));
    callback.setToSuccess();
  }

  public void config(String line, DispatchCallback callback) {
    try {
      Properties props = sqlLine.getOpts().toProperties();
      Set<String> keys = new TreeSet<>(asMap(props).keySet());
      for (String key : keys) {
        sqlLine.outputProperty(
            key.substring(SqlLineOpts.PROPERTY_PREFIX.length()),
            props.getProperty(key));
      }
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
      return;
    }

    callback.setToSuccess();
  }

  public void set(String line, DispatchCallback callback) {
    if (line == null || line.trim().equals("set")
        || line.length() == 0) {
      config(null, callback);
      return;
    }

    final String[] parts = sqlLine.split(line);
    if (parts.length > 3) {
      sqlLine.error("Usage: set [all | <property name> [<value>]]");
      callback.setToFailure();
      return;
    }

    String propertyName = parts[1].toLowerCase(Locale.ROOT);

    if ("all".equals(propertyName)) {
      config(null, callback);
      return;
    }

    if (!sqlLine.getOpts().hasProperty(propertyName)) {
      sqlLine.error(sqlLine.loc("no-specified-prop", propertyName));
      callback.setToFailure();
      return;
    }

    if (parts.length == 2) {
      try {
        sqlLine.outputProperty(propertyName,
            sqlLine.getOpts().get(propertyName));
        callback.setToSuccess();
      } catch (Exception e) {
        sqlLine.error(e);
        callback.setToFailure();
      }
    } else {
      setProperty(propertyName, parts[2], null, callback);
    }
  }

  public void reset(String line, DispatchCallback callback) {
    String[] split = sqlLine.split(line, 2,
        "Usage: reset (all | <property name>)");
    if (split == null) {
      callback.setToFailure();
      return;
    }

    String propertyName = split[1].toLowerCase(Locale.ROOT);

    if ("all".equals(propertyName)) {
      sqlLine.setOpts(new SqlLineOpts(sqlLine));
      sqlLine.output(sqlLine.loc("reset-all-props"));
      // no need to auto save, since its off by default
      callback.setToSuccess();
      return;
    }

    if (!sqlLine.getOpts().hasProperty(propertyName)) {
      sqlLine.error(sqlLine.loc("no-specified-prop", propertyName));
      callback.setToFailure();
      return;
    }

    try {
      String defaultValue = new SqlLineOpts(sqlLine).get(propertyName);
      setProperty(propertyName, defaultValue, "reset-prop", callback);
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  private void setProperty(String key, String value, String res,
      DispatchCallback callback) {
    boolean success = sqlLine.getOpts().set(key, value, false);
    if (success) {
      if (sqlLine.getOpts().getAutoSave()) {
        try {
          sqlLine.getOpts().save();
        } catch (Exception saveException) {
          // ignore
        }
      }
      if (res != null) {
        sqlLine.output(sqlLine.loc(res, key, value));
      }
      callback.setToSuccess();
    } else {
      callback.setToFailure();
    }
  }

  private void reportResult(String action, long start, long end) {
    if (sqlLine.getOpts().getShowElapsedTime()) {
      sqlLine.info(action + " " + sqlLine.locElapsedTime(end - start));
    } else {
      sqlLine.info(action);
    }
  }

  public void commit(String line, DispatchCallback callback) {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }
    if (!(sqlLine.assertAutoCommit())) {
      callback.setToFailure();
      return;
    }

    try {
      long start = System.currentTimeMillis();
      sqlLine.getDatabaseConnection().connection.commit();
      long end = System.currentTimeMillis();
      sqlLine.showWarnings();
      reportResult(sqlLine.loc("commit-complete"), start, end);

      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  public void rollback(String line, DispatchCallback callback) {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }
    if (!(sqlLine.assertAutoCommit())) {
      callback.setToFailure();
      return;
    }

    try {
      long start = System.currentTimeMillis();
      sqlLine.getDatabaseConnection().connection.rollback();
      long end = System.currentTimeMillis();
      sqlLine.showWarnings();
      reportResult(sqlLine.loc("rollback-complete"), start, end);
      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  public void autocommit(String line, DispatchCallback callback)
      throws SQLException {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }

    if (line.endsWith("on")) {
      sqlLine.getDatabaseConnection().connection.setAutoCommit(true);
    } else if (line.endsWith("off")) {
      sqlLine.getDatabaseConnection().connection.setAutoCommit(false);
    }

    sqlLine.showWarnings();
    sqlLine.autocommitStatus(sqlLine.getDatabaseConnection().connection);
    callback.setToSuccess();
  }

  public void readonly(String line, DispatchCallback callback)
      throws SQLException {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }

    if (line.endsWith("on")) {
      sqlLine.getDatabaseConnection().connection.setReadOnly(true);
    } else if (line.endsWith("off")) {
      sqlLine.getDatabaseConnection().connection.setReadOnly(false);
    }

    sqlLine.showWarnings();
    sqlLine.readonlyStatus(sqlLine.getDatabaseConnection().connection);
    callback.setToSuccess();
  }

  public void dbinfo(String line, DispatchCallback callback) {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }

    sqlLine.showWarnings();
    int padlen = 50;

    for (String method : METHODS) {
      try {
        final String s =
            String.valueOf(sqlLine.getReflector()
                .invoke(sqlLine.getDatabaseMetaData(), method));
        sqlLine.output(
            new AttributedStringBuilder()
                .append(rpad(method, padlen))
                .append(s)
                .toAttributedString());
      } catch (Exception e) {
        sqlLine.handleException(e);
      }
    }

    callback.setToSuccess();
  }

  public void verbose(String line, DispatchCallback callback) {
    sqlLine.info("verbose: on");
    set("set verbose true", callback);
  }

  public void outputformat(String line, DispatchCallback callback) {
    try {
      String[] lines = sqlLine.split(line);
      sqlLine.getOpts().setOutputFormat(lines[1]);
      if ("csv".equals(lines[1])) {
        if (lines.length > 2) {
          sqlLine.getOpts().set(BuiltInProperty.CSV_DELIMITER, lines[2]);
        }
        if (lines.length > 3) {
          sqlLine.getOpts().setCsvQuoteCharacter(lines[3]);
        }
      } else if ("table".equals(lines[1])) {
        if (lines.length > 2) {
          sqlLine.getOpts().set(BuiltInProperty.MAX_COLUMN_WIDTH, lines[2]);
        }
      }
      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
    }
  }

  public void brief(String line, DispatchCallback callback) {
    sqlLine.info("verbose: off");
    set("set verbose false", callback);
  }

  public void isolation(String line, DispatchCallback callback)
      throws SQLException {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }

    final int i;
    line = line.toUpperCase(Locale.ROOT);
    if (line.endsWith(SqlLineProperty.DEFAULT.toUpperCase(Locale.ROOT))) {
      i = sqlLine.getDatabaseMetaData().getDefaultTransactionIsolation();
    } else if (line.endsWith("TRANSACTION_NONE")) {
      i = Connection.TRANSACTION_NONE;
    } else if (line.endsWith("TRANSACTION_READ_COMMITTED")) {
      i = Connection.TRANSACTION_READ_COMMITTED;
    } else if (line.endsWith("TRANSACTION_READ_UNCOMMITTED")) {
      i = Connection.TRANSACTION_READ_UNCOMMITTED;
    } else if (line.endsWith("TRANSACTION_REPEATABLE_READ")) {
      i = Connection.TRANSACTION_REPEATABLE_READ;
    } else if (line.endsWith("TRANSACTION_SERIALIZABLE")) {
      i = Connection.TRANSACTION_SERIALIZABLE;
    } else {
      callback.setToFailure();
      sqlLine.error("Usage: isolation <TRANSACTION_NONE "
          + "| TRANSACTION_READ_COMMITTED "
          + "| TRANSACTION_READ_UNCOMMITTED "
          + "| TRANSACTION_REPEATABLE_READ "
          + "| TRANSACTION_SERIALIZABLE "
          + "| DEFAULT>");
      return;
    }

    if (!sqlLine.getDatabaseMetaData().supportsTransactionIsolationLevel(i)) {
      callback.setToFailure();
      final int defaultTransactionIsolation =
          sqlLine.getDatabaseMetaData().getDefaultTransactionIsolation();
      sqlLine.error(
          sqlLine.loc("isolation-level-not-supported",
              getTransactionIsolationName(i),
              getTransactionIsolationName(defaultTransactionIsolation)));
      return;
    }

    Connection connection = sqlLine.getDatabaseConnection().getConnection();
    connection.setTransactionIsolation(i);

    sqlLine.debug(
        sqlLine.loc("isolation-status", getTransactionIsolationName(i)));
    callback.setToSuccess();
  }

  private String getTransactionIsolationName(int i) {
    switch (i) {
    case Connection.TRANSACTION_NONE:
      return "TRANSACTION_NONE";
    case Connection.TRANSACTION_READ_COMMITTED:
      return "TRANSACTION_READ_COMMITTED";
    case Connection.TRANSACTION_READ_UNCOMMITTED:
      return "TRANSACTION_READ_UNCOMMITTED";
    case Connection.TRANSACTION_REPEATABLE_READ:
      return "TRANSACTION_REPEATABLE_READ";
    case Connection.TRANSACTION_SERIALIZABLE:
      return "TRANSACTION_SERIALIZABLE";
    default:
      return "UNKNOWN";
    }
  }

  public void batch(String line, DispatchCallback callback) {
    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }

    if (sqlLine.getBatch() == null) {
      sqlLine.setBatch(new LinkedList<>());
      sqlLine.info(sqlLine.loc("batch-start"));
      callback.setToSuccess();
    } else {
      sqlLine.info(sqlLine.loc("running-batch"));
      try {
        sqlLine.runBatch(sqlLine.getBatch());
        callback.setToSuccess();
      } catch (Exception e) {
        callback.setToFailure();
        sqlLine.error(e);
      } finally {
        sqlLine.setBatch(null);
      }
    }
  }

  public void sql(String line, DispatchCallback callback) {
    execute(line, false, callback);
  }

  public void call(String line, DispatchCallback callback) {
    execute(line, true, callback);
  }

  private void execute(String line, boolean call, DispatchCallback callback) {
    if (line == null || line.length() == 0) {
      callback.setStatus(DispatchCallback.Status.FAILURE);
      return;
    }

    if (!sqlLine.assertConnection()) {
      callback.setToFailure();
      return;
    }

    String fullLine = line;
    if (fullLine.startsWith(SqlLine.COMMAND_PREFIX)) {
      fullLine = fullLine.substring(1);
    }

    final String prefix = call ? "call" : "sql";
    if (fullLine.startsWith(prefix)) {
      fullLine = fullLine.substring(prefix.length());
    }

    final StringBuilder sql2execute = new StringBuilder();
    for (String sqlItem : fullLine.split(";")) {
      sql2execute.append(sqlItem).append(";");
      if (sqlLine.isOneLineComment(sql2execute.toString())
          || isSqlContinuationRequired(sql2execute.toString())) {
        continue;
      }
      final String sql = skipLast(flush(sql2execute));
      executeSingleQuery(sql, call, callback);
    }
    if (!callback.isFailure()) {
      callback.setToSuccess();
    }
  }

  /** Returns the contents of a {@link StringBuilder} and clears the builder. */
  static String flush(StringBuilder buf) {
    final String s = buf.toString();
    buf.setLength(0);
    return s;
  }

  /** Returns a string with the last character removed. */
  private static String skipLast(String s) {
    return s.substring(0, s.length() - 1);
  }

  private void executeSingleQuery(String sql, boolean call,
      DispatchCallback callback) {
    // batch statements?
    if (sqlLine.getBatch() != null) {
      sqlLine.getBatch().add(sql);
      return;
    }

    try {
      Statement stmnt = null;
      boolean hasResults;

      try {
        final long start = System.currentTimeMillis();
        if (sqlLine.getOpts().getCompiledConfirmPattern().matcher(sql).find()
            && sqlLine.getOpts().getConfirm()) {
          final String question = sqlLine.loc("really-perform-action");
          final int userResponse =
              getUserAnswer(question, 'y', 'n', 'Y', 'N');
          if (userResponse != 'y' && userResponse != 'Y') {
            sqlLine.error(sqlLine.loc("abort-action"));
            callback.setToFailure();
            return;
          }
        }
        if (call) {
          stmnt = sqlLine.getDatabaseConnection().connection.prepareCall(sql);
          callback.trackSqlQuery(stmnt);
          hasResults = ((CallableStatement) stmnt).execute();
        } else {
          stmnt = sqlLine.createStatement();
          callback.trackSqlQuery(stmnt);
          hasResults = stmnt.execute(sql);
        }

        sqlLine.showWarnings();
        sqlLine.showWarnings(stmnt.getWarnings());

        if (hasResults) {
          do {
            try (ResultSet rs = stmnt.getResultSet()) {
              int count = sqlLine.print(rs, callback);
              long end = System.currentTimeMillis();

              reportResult(sqlLine.loc("rows-selected", count), start, end);
            }
          } while (SqlLine.getMoreResults(stmnt));
        } else {
          int count = stmnt.getUpdateCount();
          long end = System.currentTimeMillis();
          reportResult(sqlLine.loc("rows-affected", count), start, end);
        }
      } finally {
        if (stmnt != null) {
          sqlLine.showWarnings(stmnt.getWarnings());
          stmnt.close();
        }
      }
    } catch (UserInterruptException uie) {
      // CTRL-C'd out of the command. Note it, but don't call it an
      // error.
      callback.setStatus(DispatchCallback.Status.CANCELED);
      sqlLine.output(sqlLine.loc("command-canceled"));
      return;
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }

    sqlLine.showWarnings();
  }

  public void quit(String line, DispatchCallback callback) {
    sqlLine.setExit(true);
    close(null, callback);
  }

  /**
   * Closes all connections.
   *
   * @param line Command line
   * @param callback Callback for command status
   */
  public void closeall(String line, DispatchCallback callback) {
    close(null, callback);
    if (callback.isSuccess()) {
      while (callback.isSuccess()) {
        close(null, callback);
      }
      // the last "close" will set it to fail so reset it to success.
      callback.setToSuccess();
    }
    // probably a holdover of the old boolean returns.
    callback.setToFailure();
  }

  /**
   * Closes the current connection.
   * Closes the current file writer.
   *
   * @param line Command line
   * @param callback Callback for command status
   */
  public void close(String line, DispatchCallback callback) {
    // close file writer
    if (sqlLine.getRecordOutputFile() != null) {
      // instead of line could be any string
      stopRecording(line, callback);
    }

    DatabaseConnection databaseConnection = sqlLine.getDatabaseConnection();
    if (databaseConnection == null) {
      callback.setToFailure();
      return;
    }

    try {
      Connection connection = databaseConnection.getConnection();
      if (connection != null && !connection.isClosed()) {
        sqlLine.debug(
            sqlLine.loc("closing", connection.getClass().getName()));
        connection.close();
      } else {
        sqlLine.debug(sqlLine.loc("already-closed"));
      }
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
      return;
    }

    sqlLine.getDatabaseConnections().remove();
    callback.setToSuccess();
  }

  /**
   * Connects to the database defined in the specified properties file.
   *
   * @param line Command line
   * @param callback Callback for command status
   * @throws Exception on error
   */
  public void properties(String line, DispatchCallback callback)
      throws Exception {
    String example = "";
    example += "Usage: properties <properties file>" + SqlLine.getSeparator();

    String[] parts = sqlLine.split(line);
    if (parts.length < 2) {
      callback.setToFailure();
      sqlLine.error(example);
      return;
    }

    int successes = 0;

    for (int i = 1; i < parts.length; i++) {
      Properties props = new Properties();
      props.load(new FileInputStream(parts[i]));
      connect(props, callback);
      if (callback.isSuccess()) {
        successes++;
        String nickname = getProperty(props, "nickname", "ConnectionNickname");
        if (nickname != null) {
          sqlLine.getDatabaseConnection().setNickname(nickname);
        }
      }
    }

    if (successes != parts.length - 1) {
      callback.setToFailure();
    } else {
      callback.setToSuccess();
    }
  }

  public void connect(String line, DispatchCallback callback) {
    String example =
        "Usage: connect [-p property value]* <url> [username] [password] [driver]"
        + SqlLine.getSeparator();
    String[] parts = sqlLine.split(line);
    if (parts == null) {
      callback.setToFailure();
      return;
    }

    Properties connectProps = new Properties();
    int offset = 1;
    for (int i = 1; i < parts.length; i++) {
      if ("-p".equals(parts[i])) {
        if (parts.length - i > 2) {
          connectProps.setProperty(parts[i + 1], parts[i + 2]);
          i = i + 2;
          offset += 3;
        } else {
          callback.setToFailure();
          sqlLine.error(example);
          return;
        }
      }
    }

    String url = parts.length < offset + 1 ? null : parts[offset];
    String user = parts.length < offset + 2 ? null : parts[offset + 1];
    String pass = parts.length < offset + 3 ? null : parts[offset + 2];
    String driver = parts.length < offset + 4 ? null : parts[offset + 3];
    Properties props = new Properties();
    if (url != null) {
      props.setProperty("url", url);
    }
    if (driver != null) {
      props.setProperty("driver", driver);
    }
    if (user != null) {
      props.setProperty("user", user);
    }
    if (pass != null) {
      props.setProperty("password", pass);
    }
    if (!connectProps.isEmpty()) {
      props.put(CONNECT_PROPERTY, connectProps);
    }
    connect(props, callback);
  }

  public void nickname(String line, DispatchCallback callback) {
    String example = "Usage: nickname <nickname for current connection>"
        + SqlLine.getSeparator();

    String[] parts = sqlLine.split(line);
    if (parts == null) {
      callback.setToFailure();
      sqlLine.error(example);
      return;
    }

    String nickname = parts.length < 2 ? null : parts[1];
    if (nickname != null) {
      DatabaseConnection current = sqlLine.getDatabaseConnection();
      if (current != null) {
        current.setNickname(nickname);
        callback.setToSuccess();
      } else {
        sqlLine.error("nickname command requires active connection");
      }
    } else {
      sqlLine.error(example);
    }
  }

  private String getProperty(Properties props, String... keys) {
    for (String key : keys) {
      String val = props.getProperty(key);
      if (val != null) {
        return val;
      }
    }

    for (String key : asMap(props).keySet()) {
      for (String key1 : keys) {
        if (key.endsWith(key1)) {
          return props.getProperty(key);
        }
      }
    }

    return null;
  }

  public void connect(Properties props, DispatchCallback callback) {
    String url = getProperty(props,
        "url",
        "javax.jdo.option.ConnectionURL",
        "ConnectionURL");
    String driver = getProperty(props,
        "driver",
        "javax.jdo.option.ConnectionDriverName",
        "ConnectionDriverName");
    String username = getProperty(props,
        "user",
        "javax.jdo.option.ConnectionUserName",
        "ConnectionUserName");
    String password = getProperty(props,
        "password",
        "javax.jdo.option.ConnectionPassword",
        "ConnectionPassword");
    Properties info = (Properties) props.get(CONNECT_PROPERTY);
    if (info != null) {
      url = url == null ? info.getProperty("url") : url;
      driver = driver == null ? info.getProperty("driver") : driver;
      username = username == null ? info.getProperty("user") : username;
      password = password == null ? info.getProperty("password") : password;
    }
    final String connectInteractionMode =
        sqlLine.getOpts().get(BuiltInProperty.CONNECT_INTERACTION_MODE);
    if (isBlank(username)
        && isBlank(password)
        && "useNPTogetherOrEmpty".equals(connectInteractionMode)) {
      username = password = "";
    }
    if (url == null || url.length() == 0) {
      callback.setToFailure();
      sqlLine.error(sqlLine.loc("no-url"));
      return;
    }
    if (driver == null || driver.length() == 0) {
      if (sqlLine.scanForDriver(url) == null) {
        callback.setToFailure();
        sqlLine.error(sqlLine.loc("no-driver", url));
        return;
      }
    } else {
      try {
        Class.forName(driver);
      } catch (ClassNotFoundException cnfe) {
        String specifiedDriver = driver;
        if ((driver = sqlLine.scanForDriver(url)) == null) {
          callback.setToFailure();
          sqlLine.error(sqlLine.loc("no-specified-driver", specifiedDriver));
          return;
        }
        sqlLine.info(
            sqlLine.loc("no-specified-driver-use-existing", specifiedDriver,
                driver));
      }
    }

    sqlLine.debug("Connecting to " + url);
    if (!"notAskCredentials".equals(connectInteractionMode)) {
      if (username == null) {
        username = readUsername(url);
        username = isBlank(username) ? null : username;
      }
      if (password == null) {
        password = readPassword(url);
        password = isBlank(password) ? null : password;
      }
    }
    DatabaseConnection connection =
        new DatabaseConnection(sqlLine, driver, url, username, password, info);
    try {
      sqlLine.getDatabaseConnections().setConnection(connection);
      sqlLine.getDatabaseConnection().getConnection();
      callback.setToSuccess();
    } catch (Exception e) {
      connection.close();
      sqlLine.getDatabaseConnections().removeConnection(connection);
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  /** Returns whether a string is null or empty. */
  private boolean isBlank(String s) {
    return s == null || s.isEmpty();
  }

  String readUsername(String url) {
    return sqlLine.withPrompting(() -> sqlLine.getLineReader()
        .readLine("Enter username for " + url + ": "));
  }

  String readPassword(String url) {
    return sqlLine.withPrompting(() -> sqlLine.getLineReader()
        .readLine("Enter password for " + url + ": ",
            null, new MaskingCallbackImpl('*'), null));
  }

  public void rehash(String line, DispatchCallback callback) {
    try {
      if (!sqlLine.assertConnection()) {
        callback.setToFailure();
      }

      if (sqlLine.getDatabaseConnection() != null) {
        sqlLine.getDatabaseConnection().setCompletions(false);
      }

      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  void resize() {
    final Terminal terminal = sqlLine.getTerminal();
    if (terminal == null
        || terminal.getWidth() == 0 || terminal.getHeight() == 0) {
      return;
    }

    sqlLine.getOpts().set(BuiltInProperty.MAX_HEIGHT, terminal.getHeight());
    sqlLine.getOpts().set(BuiltInProperty.MAX_WIDTH, terminal.getWidth());
    sqlLine.debug(sqlLine.loc(
        "new-size-after-resize", terminal.getHeight(), terminal.getWidth()));
  }

  public void resize(String line, DispatchCallback callback) {
    try {
      resize();
      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  /**
   * Lists the current connections.
   *
   * @param line Command line
   * @param callback Callback for command status
   */
  public void list(String line, DispatchCallback callback) {
    int index = 0;
    DatabaseConnections databaseConnections = sqlLine.getDatabaseConnections();
    sqlLine.info(
        sqlLine.loc("active-connections", databaseConnections.size()));

    for (DatabaseConnection databaseConnection : databaseConnections) {
      boolean closed;
      try {
        closed = databaseConnection.connection.isClosed();
      } catch (Exception e) {
        closed = true;
      }

      sqlLine.output(rpad(" #" + index++ + "", 5)
          + rpad(closed ? sqlLine.loc("closed") : sqlLine.loc("open"), 9)
          + rpad(databaseConnection.getNickname(), 20)
          + " " + databaseConnection.getUrl());
    }

    callback.setToSuccess();
  }

  public void all(String line, DispatchCallback callback) {
    int index = sqlLine.getDatabaseConnections().getIndex();
    boolean success = true;
    for (int i = 0; i < sqlLine.getDatabaseConnections().size(); i++) {
      sqlLine.getDatabaseConnections().setIndex(i);
      sqlLine.output(
          sqlLine.loc("executing-con", sqlLine.getDatabaseConnection()));

      // ### FIXME:  this is broken for multi-line SQL
      sql(line.substring("all ".length()), callback);
      success = callback.isSuccess() && success;
    }

    // restore index
    sqlLine.getDatabaseConnections().setIndex(index);
    if (success) {
      callback.setToSuccess();
    } else {
      callback.setToFailure();
    }
  }

  public void go(String line, DispatchCallback callback) {
    String[] parts = sqlLine.split(line, 2, "Usage: go <connection index>");
    if (parts == null) {
      callback.setToFailure();
      return;
    }

    boolean isNumber;
    int index = Integer.MIN_VALUE;
    try {
      index = Integer.parseInt(parts[1]);
      isNumber = true;
    } catch (Exception e) {
      isNumber = false;
    }
    if (!isNumber || !sqlLine.getDatabaseConnections().setIndex(index)) {
      sqlLine.error(sqlLine.loc("invalid-connection", parts[1]));
      list("", callback); // list the current connections
      callback.setToFailure();
      return;
    }

    callback.setToSuccess();
  }

  /**
   * Starts or stops saving a script to a file.
   *
   * @param line Command line
   * @param callback Callback for command status
   */
  public void script(String line, DispatchCallback callback) {
    if (sqlLine.getScriptOutputFile() == null) {
      startScript(line, callback);
    } else {
      stopScript(line, callback);
    }
  }

  /**
   * Stop writing to the script file and close the script.
   */
  private void stopScript(String line, DispatchCallback callback) {
    try {
      sqlLine.getScriptOutputFile().close();
    } catch (Exception e) {
      sqlLine.handleException(e);
    }

    sqlLine.output(sqlLine.loc("script-closed", sqlLine.getScriptOutputFile()));
    sqlLine.setScriptOutputFile(null);
    callback.setToSuccess();
  }

  /**
   * Start writing to the specified script file.
   */
  private void startScript(String line, DispatchCallback callback) {
    OutputFile outFile = sqlLine.getScriptOutputFile();
    if (outFile != null) {
      callback.setToFailure();
      sqlLine.error(sqlLine.loc("script-already-running", outFile));
      return;
    }

    String filename;
    if (line.length() == "script".length()
        || (filename =
            sqlLine.dequote(line.substring("script".length() + 1))) == null) {
      sqlLine.error("Usage: script <file name>");
      callback.setToFailure();
      return;
    }

    try {
      outFile = new OutputFile(expand(filename));
      sqlLine.setScriptOutputFile(outFile);
      sqlLine.output(sqlLine.loc("script-started", outFile));
      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  /**
   * Runs a script from the specified file.
   *
   * @param line Command line
   * @param callback Callback for command status
   */
  public void run(String line, DispatchCallback callback) {
    String filename;
    if (line.length() == "run".length()
        || (filename =
            sqlLine.dequote(line.substring("run".length() + 1))) == null) {
      sqlLine.error("Usage: run <file name>");
      callback.setToFailure();
      return;
    }
    List<String> cmds = new LinkedList<>();

    try {
      try (BufferedReader reader = new BufferedReader(
          new InputStreamReader(
              new FileInputStream(expand(filename)), StandardCharsets.UTF_8))) {
        // ### NOTE: fix for sf.net bug 879427
        final StringBuilder cmd = new StringBuilder();
        boolean needsContinuation;
        for (;;) {
          final String scriptLine = reader.readLine();
          if (scriptLine == null) {
            break;
          }
          // we're continuing an existing command
          cmd.append(" \n");
          cmd.append(scriptLine);

          needsContinuation = isSqlContinuationRequired(cmd.toString());
          if (!needsContinuation && !cmd.toString().trim().isEmpty()) {
            cmds.add(maybeTrim(flush(cmd)));
          }
        }

        if (SqlLineParser.isSql(sqlLine, cmd.toString(),
            Parser.ParseContext.ACCEPT_LINE)) {
          // ### REVIEW: oops, somebody left the last command
          // unterminated; should we fix it for them or complain?
          // For now be nice and fix it.
          cmd.append(";");
          cmds.add(cmd.toString());
        }
      }

      // success only if all the commands were successful
      if (sqlLine.runCommands(cmds, callback) == cmds.size()) {
        callback.setToSuccess();
      } else {
        callback.setToFailure();
      }
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  /** Returns a line, trimmed if the
   * {@link BuiltInProperty#TRIM_SCRIPTS options require trimming}. */
  private String maybeTrim(String line) {
    return sqlLine.getOpts().getTrimScripts() ? line.trim() : line;
  }

  /** Expands "~" to the home directory.
   *
   * @param filename File name
   * @return Expanded file name
   */
  public static String expand(String filename) {
    if (filename.startsWith("~" + File.separator)) {
      try {
        String home = System.getProperty("user.home");
        if (home != null) {
          return home + filename.substring(1);
        }
      } catch (SecurityException e) {
        // ignore
      }
    }
    return filename;
  }

  /**
   * Starts or stops saving all output to a file.
   *
   * @param line Command line
   * @param callback Callback for command status
   */
  public void record(String line, DispatchCallback callback) {
    if (sqlLine.getRecordOutputFile() == null) {
      startRecording(line, callback);
    } else {
      stopRecording(line, callback);
    }
  }

  public void commandhandler(String line, DispatchCallback callback) {
    String[] cmd = sqlLine.split(line);
    if (cmd.length < 2) {
      sqlLine.error("Usage: commandhandler "
          + "<commandHandler class name> [<commandHandler class name>]*");
      callback.setToFailure();
      return;
    }

    final List<CommandHandler> commandHandlers =
        new ArrayList<CommandHandler>(sqlLine.getCommandHandlers());
    final Set<String> existingNames = new HashSet<>();
    for (CommandHandler existingCommandHandler : commandHandlers) {
      existingNames.addAll(existingCommandHandler.getNames());
    }

    int commandHandlerUpdateCount = 0;
    for (int i = 1; i < cmd.length; i++) {
      try {
        @SuppressWarnings("unchecked")
        Class<CommandHandler> commandHandlerClass =
            (Class<CommandHandler>) Class.forName(cmd[i]);
        final Constructor<CommandHandler> constructor =
            commandHandlerClass.getConstructor(SqlLine.class);
        CommandHandler commandHandler = constructor.newInstance(sqlLine);
        if (intersects(existingNames, commandHandler.getNames())) {
          sqlLine.error("Could not add command handler " + cmd[i] + " as one "
              + "of commands " + commandHandler.getNames() + " is already present");
        } else {
          commandHandlers.add(commandHandler);
          existingNames.addAll(commandHandler.getNames());
          ++commandHandlerUpdateCount;
        }
      } catch (Exception e) {
        sqlLine.error(e);
        callback.setToFailure();
      }
    }

    if (commandHandlerUpdateCount > 0) {
      sqlLine.updateCommandHandlers(commandHandlers);
    }

    if (!callback.isFailure()) {
      callback.setToSuccess();
    }
  }

  private static <E> boolean intersects(Collection<E> c1, Collection<E> c2) {
    for (E e : c2) {
      if (c1.contains(e)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Stop writing output to the record file.
   */
  private void stopRecording(String line, DispatchCallback callback) {
    try {
      sqlLine.getRecordOutputFile().close();
    } catch (Exception e) {
      sqlLine.handleException(e);
    }

    sqlLine.output(sqlLine.loc("record-closed", sqlLine.getRecordOutputFile()));
    sqlLine.setRecordOutputFile(null);
    callback.setToSuccess();
  }

  /**
   * Start writing to the specified record file.
   */
  private void startRecording(String line, DispatchCallback callback) {
    OutputFile outputFile = sqlLine.getRecordOutputFile();
    if (outputFile != null) {
      callback.setToFailure();
      sqlLine.error(sqlLine.loc("record-already-running", outputFile));
      return;
    }
    String[] cmd = sqlLine.split(line);
    if (cmd.length != 2) {
      sqlLine.error("Usage: record <file name>");
      callback.setToFailure();
      return;
    }

    final String filename = cmd[1];

    try {
      outputFile = new OutputFile(expand(filename));
      sqlLine.setRecordOutputFile(outputFile);
      sqlLine.output(sqlLine.loc("record-started", outputFile));
      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error(e);
    }
  }

  public void describe(String line, DispatchCallback callback)
      throws SQLException {
    String[][] cmd = sqlLine.splitCompound(line);
    if (cmd.length != 2) {
      sqlLine.error("Usage: describe <table name>");
      callback.setToFailure();
      return;
    }

    if (cmd[1].length == 1
        && cmd[1][0] != null
        && cmd[1][0].equalsIgnoreCase("tables")) {
      tables("tables", callback);
    } else {
      columns(line, callback);
    }
  }

  public void help(String line, DispatchCallback callback) {
    String[] parts = sqlLine.split(line);
    String cmd = parts.length > 1 ? parts[1] : "";
    TreeSet<String> clist = new TreeSet<>();

    for (CommandHandler commandHandler : sqlLine.getCommandHandlers()) {
      if (cmd.length() == 0
          || commandHandler.getNames().contains(cmd)) {
        String help = commandHandler.getHelpText();
        help = sqlLine.wrap(help, 60, 20);
        if (cmd.equals("set")) {
          help += sqlLine.loc("variables");
        }
        clist.add(rpad(SqlLine.COMMAND_PREFIX + commandHandler.getName(), 20)
            + help);
      }
    }

    for (String c : clist) {
      sqlLine.output(c);
    }

    if (cmd.length() == 0) {
      sqlLine.output(sqlLine.loc("variables"));
      sqlLine.output("");
      sqlLine.output(
          sqlLine.loc("comments", SqlLine.getApplicationContactInformation()));
    }

    callback.setToSuccess();
  }

  public void manual(String line, DispatchCallback callback)
      throws IOException {
    InputStream in = SqlLine.class.getResourceAsStream("manual.txt");
    if (in == null) {
      callback.setToFailure();
      sqlLine.error(sqlLine.loc("no-manual"));
      return;
    }

    // Workaround for windows because of
    // https://github.com/jline/jline3/issues/304
    if (System.getProperty("os.name")
        .toLowerCase(Locale.ROOT).contains("windows")) {
      sillyLess(in);
    } else {
      try {
        org.jline.builtins.Commands.less(sqlLine.getLineReader().getTerminal(),
            in, sqlLine.getOutputStream(), sqlLine.getErrorStream(),
            null, new String[]{});
      } catch (Exception e) {
        callback.setToFailure();
        sqlLine.error(e);
        return;
      }
    }

    callback.setToSuccess();
  }

  private void sillyLess(InputStream in) throws IOException {
    BufferedReader reader =
        new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
    String man;
    int index = 0;
    while ((man = reader.readLine()) != null) {
      index++;
      sqlLine.output(man);

      // silly little pager
      if (index % (sqlLine.getOpts().getMaxHeight() - 1) == 0) {
        final int userInput =
            getUserAnswer(sqlLine.loc("enter-for-more"), 'q', 13);
        if (userInput == -1 || userInput == 'q') {
          sqlLine.getLineReader().getTerminal().writer().write('\n');
          break;
        }
      }
    }

    reader.close();
  }

  public void appconfig(String line, DispatchCallback callback) {
    String example =
        "Usage: appconfig <class name for application configuration>"
        + SqlLine.getSeparator();

    String[] parts = sqlLine.split(line);
    if (parts == null || parts.length != 2) {
      callback.setToFailure();
      sqlLine.error(example);
      return;
    }

    try {
      Application appConfig = (Application) Class.forName(parts[1])
          .getConstructor().newInstance();
      sqlLine.setAppConfig(appConfig);
      callback.setToSuccess();
    } catch (Exception e) {
      callback.setToFailure();
      sqlLine.error("Could not initialize " + parts[1]);
    }
  }

  public void prompthandler(String line, DispatchCallback callback) {
    String example =
        "Usage: prompthandler <prompt handler class name>"
          + SqlLine.getSeparator();

    String[] parts = sqlLine.split(line);
    if (parts == null || parts.length != 2) {
      callback.setToFailure();
      sqlLine.error(example);
      return;
    }

    String className = parts[1];

    PromptHandler promptHandler;
    if ("default".equalsIgnoreCase(className)) {
      promptHandler = new PromptHandler(sqlLine);
    } else {
      try {
        promptHandler = (PromptHandler) Class.forName(className)
          .getConstructor(SqlLine.class).newInstance(sqlLine);
      } catch (Exception e) {
        callback.setToFailure();
        sqlLine.error("Could not initialize " + className);
        return;
      }
    }

    sqlLine.updatePromptHandler(promptHandler);
    callback.setToSuccess();
  }

  static Map<String, String> asMap(Properties properties) {
    //noinspection unchecked
    return (Map) properties;
  }

  private boolean isSqlContinuationRequired(String sql) {
    if (sqlLine.getLineReader() == null) {
      return false;
    }
    return SqlLineParser.SqlParserState.OK
        != ((SqlLineParser) sqlLine.getLineReader().getParser())
            .parseState(sql, sql.length(), Parser.ParseContext.ACCEPT_LINE)
            .getState();
  }

  /**
   * Callback that masks input while reading a password.
   */
  private static class MaskingCallbackImpl implements MaskingCallback {
    private final Character mask;

    MaskingCallbackImpl(Character mask) {
      this.mask = Objects.requireNonNull(mask);
    }

    @Override public String display(String line) {
      if (mask.equals(LineReaderImpl.NULL_MASK)) {
        return "";
      } else {
        final StringBuilder sb = new StringBuilder(line.length());
        for (char c : line.toCharArray()) {
          if (c == '\n') {
            sb.append(c);
          } else {
            sb.append((char) mask);
          }
        }
        return sb.toString();
      }
    }

    @Override public String history(String line) {
      return null;
    }
  }
}

// End Commands.java