Package org.hsqldb.cmdline

Source Code of org.hsqldb.cmdline.SqlFile$BadSubst

/* Copyright (c) 2001-2010, The HSQL Development Group
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the HSQL Development Group nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


package org.hsqldb.cmdline;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.logging.Level;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import org.hsqldb.lib.AppendableException;
import org.hsqldb.lib.RCData;
import org.hsqldb.lib.StringUtil;
import org.hsqldb.lib.FrameworkLogger;
import org.hsqldb.cmdline.sqltool.Token;
import org.hsqldb.cmdline.sqltool.TokenList;
import org.hsqldb.cmdline.sqltool.TokenSource;
import org.hsqldb.cmdline.sqltool.SqlFileScanner;

/* $Id: SqlFile.java 3628 2010-06-06 00:44:08Z unsaved $ */

/**
* Encapsulation of SQL text and the environment under which it will executed
* with a JDBC Connection.
* 'SqlInputStream' would be a more precise name, but the content we are
* talking about here is what is colloqially known as the contents of
* "SQL file"s.
* <P>
* The file <CODE>src/org/hsqldb/sample/SqlFileEmbedder.java</CODE>
* in the HSQLDB distribution provides an example for using SqlFile to
* execute SQL files directly from your own Java classes.
* <P/><P>
* The complexities of passing userVars and macros maps are to facilitate
* strong scoping (among blocks and nested scripts).
* <P/><P>
* Some implementation comments and variable names use keywords based
* on the following definitions.  <UL>
* <LI> COMMAND = Statement || SpecialCommand || BufferCommand
* <LI>Statement = SQL statement like "SQL Statement;"
* <LI>SpecialCommand =  Special Command like "\x arg..."
* <LI>BufferCommand =  Editing/buffer command like ":s/this/that/"
* </UL>
* <P/><P>
* When entering SQL statements, you are always "appending" to the
* "immediate" command (not the "buffer", which is a different thing).
* All you can do to the immediate command is append new lines to it,
* execute it, or save it to buffer.
* When you are entering a buffer edit command like ":s/this/that/",
* your immediate command is the buffer-edit-command.  The buffer
* is the command string that you are editing.
* The buffer usually contains either an exact copy of the last command
* executed or sent to buffer by entering a blank line,
* but BUFFER commands can change the contents of the buffer.
* <P/><P>
* In general, the special commands mirror those of Postgresql's psql,
* but SqlFile handles command editing very differently than Postgresql
* does, in part because of Java's lack of support for raw tty I/O.
* The \p special command, in particular, is very different from psql's.
* <P/><P>
* Buffer commands are unique to SQLFile.  The ":" commands allow
* you to edit the buffer and to execute the buffer.
* <P/><P>
* \d commands are very poorly supported for Mysql because
* (a) Mysql lacks most of the most basic JDBC support elements, and
* the most basic role and schema features, and
* (b) to access the Mysql data dictionary, one must change the database
* instance (to do that would require work to restore the original state
* and could have disastrous effects upon transactions).
* <P/><P>
* The process*() methods, other than processBuffHist() ALWAYS execute
* on "buffer", and expect it to contain the method specific prefix
* (if any).
* <P/><P>
* The input/output Reader/Stream are generally managed by the caller.
* An exception is that the input reader may be closed automatically or on
* demand by the user, since in some cases this class builds the Reader.
* There is no corresponding functionality for output since the user always
* has control over that object (which may be null or System.out).
* <P/>
*
* @see <a href="../../../../util-guide/sqltool-chapt.html" target="guide">
*     The SqlTool chapter of the
*     HyperSQL Utilities Guide</a>
* @see org.hsqldb.sample.SqlFileEmbedder
* @version $Revision: 3628 $, $Date: 2010-06-05 20:44:08 -0400 (Sat, 05 Jun 2010) $
* @author Blaine Simpson (blaine dot simpson at admc dot com)
*/

public class SqlFile {
    private static FrameworkLogger logger =
            FrameworkLogger.getLog(SqlFile.class);
    private static final int DEFAULT_HISTORY_SIZE = 40;
    private boolean          executing;
    private boolean permitEmptySqlStatements;
    private boolean          interactive;
    private String           primaryPrompt    = "sql> ";
    private static String    rawPrompt;
    private static Method    createArrayOfMethod;
    private String           contPrompt       = "  +> ";
    private boolean          htmlMode;
    private TokenList        history;
    private String           nullRepToken;
    private String           dsvTargetFile;
    private String           dsvTargetTable;
    private String           dsvConstCols;
    private String           dsvRejectFile;
    private String           dsvRejectReport;
    private int              dsvRecordsPerCommit = 0;
    /** Platform-specific line separator */
    public static String     LS = System.getProperty("line.separator");
    private int              maxHistoryLength = 1;
    // TODO:  Implement PL variable to interactively change history length.
    // Study to be sure this won't cause state inconsistencies.
    private boolean          reportTimes;
    private Reader           reader;
    // Reader serves the auxiliary purpose of null meaning execute()
    // has finished.
    private String           inputStreamLabel;

    static String            DEFAULT_FILE_ENCODING =
                             System.getProperty("file.encoding");

    /**
     * N.b. javax.util.regex Optional capture groups (...)? are completely
     * unpredictable wrt whether you get a null capture group vs. no capture.
     * Must always check count!
     */
    private static Pattern   specialPattern =
            Pattern.compile("(\\S+)(?:\\s+(.*\\S))?\\s*");
    private static Pattern   plPattern  = Pattern.compile("(.*\\S)?\\s*");
    private static Pattern   foreachPattern =
            Pattern.compile("foreach\\s+(\\S+)\\s*\\(([^)]+)\\)\\s*");
    private static Pattern   ifwhilePattern =
            Pattern.compile("\\S+\\s*\\(([^)]*)\\)\\s*");
    private static Pattern   varsetPattern =
            Pattern.compile("(\\S+)\\s*([=_~])\\s*(?:(.*\\S)\\s*)?");
    private static Pattern   substitutionPattern =
            Pattern.compile("(\\S)(.+?)\\1(.*?)\\1(.+)?\\s*");
            // Note that this pattern does not include the leading ":s".
    private static Pattern   slashHistoryPattern =
            Pattern.compile("\\s*/([^/]+)/\\s*(\\S.*)?");
    private static Pattern   historyPattern =
            Pattern.compile("\\s*(-?\\d+)?\\s*(\\S.*)?");
            // Note that this pattern does not include the leading ":".
    private static Pattern wincmdPattern;
    private static Pattern useMacroPattern =
            Pattern.compile("(\\w+)(\\s.*[^;])?(;?)");
    private static Pattern editMacroPattern =
            Pattern.compile("(\\w+)\\s*:(.*)");
    private static Pattern spMacroPattern =
            Pattern.compile("(\\w+)\\s+([*\\\\])(.*\\S)");
    private static Pattern sqlMacroPattern =
            Pattern.compile("(\\w+)\\s+(.*\\S)");
    private static Pattern integerPattern = Pattern.compile("\\d+");
    private static Pattern nameValPairPattern =
            Pattern.compile("\\s*(\\w+)\\s*=(.*)");
            // Specifically permits 0-length values, but not names.
    private static Pattern dotPattern = Pattern.compile("(\\w*)\\.(\\w*)");
    private static Pattern commitOccursPattern =
            Pattern.compile("(?is)(?:set\\s+autocommit.*)|(commit\\s*)");
    private static Pattern logPattern =
        Pattern.compile("(?i)(FINER|WARNING|SEVERE|INFO|FINEST)\\s+(.*\\S)");
    private static Pattern   arrayPattern =
            Pattern.compile("ARRAY\\s*\\[\\s*(.*\\S)?\\s*\\]");

    private static Map<String, Pattern> nestingPLCommands =
            new HashMap<String, Pattern>();
    static {
        nestingPLCommands.put("if", ifwhilePattern);
        nestingPLCommands.put("while", ifwhilePattern);
        nestingPLCommands.put("foreach", foreachPattern);

        if (System.getProperty("os.name").startsWith("Windows")) {
            wincmdPattern = Pattern.compile("([^\"]+)?(\"[^\"]*\")?");
        }

        rawPrompt = SqltoolRB.rawmode_prompt.getString() + "> ";
        DSV_OPTIONS_TEXT = SqltoolRB.dsv_options.getString();
        D_OPTIONS_TEXT = SqltoolRB.d_options.getString();
        DSV_X_SYNTAX_MSG = SqltoolRB.dsv_x_syntax.getString();
        DSV_M_SYNTAX_MSG = SqltoolRB.dsv_m_syntax.getString();
        nobufferYetString = SqltoolRB.nobuffer_yet.getString();
        try {
            SqlFile.createArrayOfMethod = Connection.class.getDeclaredMethod(
                    "createArrayOf", String.class, Object[].class);
        } catch (Exception expectedException) {
            // Purposeful no-op.  Leave createArrayOfMethod null.
        }
    }
    // This can throw a runtime exception, but since the pattern
    // Strings are constant, one test run of the program will tell
    // if the patterns are good.

    /**
     * Encapsulate updating local variables which depend upon PL variables.
     * <P>
     * Right now this is called whenever the user variable map is changed.
     * It would be more efficient to do it JIT by keeping track of when
     * the vars may be "dirty" by a variable map change, and having all
     * methods that use the settings call a conditional updater, but that
     * is less reliable since there is no way to guarantee that the vars
     * are not used without checking.
     * UPDATE:  Could do what is needed by making a Map subclass with
     * overridden setters which enforce dirtiness.
     * <P/>
     */
    private void updateUserSettings() {
        dsvSkipPrefix = SqlFile.convertEscapes(
                shared.userVars.get("*DSV_SKIP_PREFIX"));
        if (dsvSkipPrefix == null) {
            dsvSkipPrefix = DEFAULT_SKIP_PREFIX;
        }
        dsvSkipCols = shared.userVars.get("*DSV_SKIP_COLS");
        dsvTrimAll = Boolean.parseBoolean(
                shared.userVars.get("*DSV_TRIM_ALL"));
        dsvColDelim = SqlFile.convertEscapes(
                shared.userVars.get("*DSV_COL_DELIM"));
        if (dsvColDelim == null) {
            dsvColDelim = SqlFile.convertEscapes(
                    shared.userVars.get("*CSV_COL_DELIM"));
        }
        if (dsvColDelim == null) {
            dsvColDelim = DEFAULT_COL_DELIM;
        }
        dsvColSplitter = shared.userVars.get("*DSV_COL_SPLITTER");
        if (dsvColSplitter == null) {
            dsvColSplitter = DEFAULT_COL_SPLITTER;
        }

        dsvRowDelim = SqlFile.convertEscapes(
                shared.userVars.get("*DSV_ROW_DELIM"));
        if (dsvRowDelim == null) {
            dsvRowDelim = SqlFile.convertEscapes(
                    shared.userVars.get("*CSV_ROW_DELIM"));
        }
        if (dsvRowDelim == null) {
            dsvRowDelim = DEFAULT_ROW_DELIM;
        }
        dsvRowSplitter = shared.userVars.get("*DSV_ROW_SPLITTER");
        if (dsvRowSplitter == null) {
            dsvRowSplitter = DEFAULT_ROW_SPLITTER;
        }

        dsvTargetFile = shared.userVars.get("*DSV_TARGET_FILE");
        if (dsvTargetFile == null) {
            dsvTargetFile = shared.userVars.get("*CSV_FILEPATH");
        }
        dsvTargetTable = shared.userVars.get("*DSV_TARGET_TABLE");
        if (dsvTargetTable == null) {
            dsvTargetTable = shared.userVars.get("*CSV_TABLENAME");
            // This just for legacy variable name.
        }

        dsvConstCols = shared.userVars.get("*DSV_CONST_COLS");
        dsvRejectFile = shared.userVars.get("*DSV_REJECT_FILE");
        dsvRejectReport = shared.userVars.get("*DSV_REJECT_REPORT");
        if (shared.userVars.get("*DSV_RECORDS_PER_COMMIT") != null) try {
            dsvRecordsPerCommit = Integer.parseInt(
                    shared.userVars.get("*DSV_RECORDS_PER_COMMIT"));
        } catch (NumberFormatException nfe) {
            logger.error(SqltoolRB.reject_rpc.getString(
                    shared.userVars.get("*DSV_RECORDS_PER_COMMIT")));
            shared.userVars.remove("*DSV_REJECT_REPORT");
            dsvRecordsPerCommit = 0;
        }

        nullRepToken = shared.userVars.get("*NULL_REP_TOKEN");
        if (nullRepToken == null) {
            nullRepToken = shared.userVars.get("*CSV_NULL_REP");
        }
        if (nullRepToken == null) {
            nullRepToken = DEFAULT_NULL_REP;
        }
    }

    /**
     * Private class to "share" attributes among a family of SqlFile instances.
     */
    private static class SharedFields {
        /* Since SqlTool can run against different versions of HSQLDB (plus
         * against any JDBC database), it can't make assumptions about
         * commands which may cause implicit commits, or commit state
         * requirements with specific databases may have for specific SQL
         * statements.  Therefore, we just assume that any statement other
         * than COMMIT or SET AUTOCOMMIT causes an implicit COMMIT (the
         * Java API spec mandates that setting AUTOCOMMIT causes an implicit
         * COMMIT, regardless of whether turning AUTOCOMMIT on or off).
         */
        boolean possiblyUncommitteds;

        Connection jdbcConn;

        Map<String, String> userVars = new HashMap<String, String>();

        Map<String, Token> macros = new HashMap<String, Token>();

        PrintStream psStd;

        SharedFields(PrintStream psStd) {
            this.psStd = psStd;
        }

        String encoding;
    }

    private SharedFields shared;

    private static final String DIVIDER =
        "-----------------------------------------------------------------"
        + "-----------------------------------------------------------------";
    // Needs to be at least as wide as the widest field or header displayed.
    private static String revnum =
            "$Revision: 3628 $".substring("$Revision: ".length(),
            "$Revision: 3628 $".length() - 2);

    private static String DSV_OPTIONS_TEXT;
    private static String D_OPTIONS_TEXT;

    /**
     * Convenience wrapper for the SqlFile(File, String) constructor
     *
     * @throws IOException
     * @see #SqlFile(File, String)
     */
    public SqlFile(File inputFile) throws IOException {
        this(inputFile, null);
    }

    /**
     * Convenience wrapper for the SqlFile(File, String, boolean) constructor
     *
     * @param encoding is applied to both the given File and other files
     *        read in or written out. Null will use your env+JVM settings.
     * @throws IOException
     * @see #SqlFile(File, String, boolean)
     */
    public SqlFile(File inputFile, String encoding) throws IOException {
        this(inputFile, encoding, false);
    }

    /**
     * Constructor for non-interactive usage with a SQL file, using the
     * specified encoding and sending normal output to stdout.
     *
     * @param encoding is applied to the given File and other files
     *        read in or written out. Null will use your env+JVM settings.
     * @param interactive  If true, prompts are printed, the interactive
     *                     Special commands are enabled, and
     *                     continueOnError defaults to true.
     * @throws IOException
     * @see #SqlFile(Reader, String, PrintStream, String, boolean)
     */
    public SqlFile(File inputFile, String encoding, boolean interactive)
            throws IOException {
        this(new InputStreamReader(new FileInputStream(inputFile),
                (encoding == null) ? DEFAULT_FILE_ENCODING : encoding),
                inputFile.toString(), System.out, encoding, interactive);
    }

    /**
     * Constructor for interactive usage with stdin/stdout
     *
     * @param encoding is applied to other files read in or written out (but
     *                     not to stdin or stdout).
     *                     Null will use your env+JVM settings.
     * @param interactive  If true, prompts are printed, the interactive
     *                     Special commands are enabled, and
     *                     continueOnError defaults to true.
     * @throws IOException
     * @see #SqlFile(Reader, String, PrintStream, String, boolean)
     */
    public SqlFile(String encoding, boolean interactive) throws IOException {
        this((encoding == null)
                ? new InputStreamReader(System.in)
                : new InputStreamReader(System.in, encoding),
                "<stdin>", System.out, encoding, interactive);
    }

    /**
     * Instantiate a SqlFile instance for SQL input from 'reader'.
     *
     * After any needed customization, the SQL can be executed by the
     * execute method.
     * <P>
     * Most Special Commands and many Buffer commands are only for
     * interactive use.
     * </P> <P>
     * This program never writes to an error stream (stderr or alternative).
     * All meta messages and error messages are written using the logging
     * facility.
     * </P>
     *
     * @param reader       Source for the SQL to be executed.
     *                     Caller is responsible for setting up encoding.
     *                     (the 'encoding' parameter will NOT be applied
     *                     to this reader).
     * @param psStd        PrintStream for normal output.
     *                     If null, normal output will be discarded.
     *                     Caller is responsible for settingup encoding
     *                     (the 'encoding' parameter will NOT be applied
     *                     to this stream).
     * @param interactive  If true, prompts are printed, the interactive
     *                     Special commands are enabled, and
     *                     continueOnError defaults to true.
     * @throws IOException
     * @see #execute()
     */
    public SqlFile(Reader reader, String inputStreamLabel,
            PrintStream psStd, String encoding, boolean interactive)
            throws IOException {
        this(reader, inputStreamLabel);
        try {
            shared = new SharedFields(psStd);
            setEncoding(encoding);
            this.interactive = interactive;
            continueOnError = this.interactive;

            if (interactive) {
                history = new TokenList();
                maxHistoryLength = DEFAULT_HISTORY_SIZE;
            }
            updateUserSettings();
            // Updates local vars basd on * shared.userVars
            // even when (like now) these are all defaults.
        } catch (IOException ioe) {
            closeReader();
            throw ioe;
        } catch (RuntimeException re) {
            closeReader();
            throw re;
        }
    }

    /**
     * Wrapper for SqlFile(SqlFile, Reader, String)
     *
     * @see #SqlFile(SqlFile, Reader, String)
     */
    private SqlFile(SqlFile parentSqlFile, File inputFile) throws IOException {
        this(parentSqlFile,
                new InputStreamReader(new FileInputStream(inputFile),
                (parentSqlFile.shared.encoding == null)
                ? DEFAULT_FILE_ENCODING : parentSqlFile.shared.encoding),
                inputFile.toString());
    }

    /**
     * Constructor for recursion
     */
    private SqlFile(SqlFile parentSqlFile, Reader reader,
            String inputStreamLabel) {
        this(reader, inputStreamLabel);
        try {
            recursed = true;
            shared = parentSqlFile.shared;
            plMode = parentSqlFile.plMode;
            interactive = false;
            continueOnError = parentSqlFile.continueOnError;
            // Nested input is non-interactive because it just can't work to
            // have user append to edit buffer, and displaying prompts would
            // be misleading and inappropriate; yet we will inherit the current
            // continueOnError behavior.
            updateUserSettings();
            // Updates local vars basd on * shared.userVars
        } catch (RuntimeException re) {
            closeReader();
            throw re;
        }
    }

    /**
     * Base Constructor which every other Constructor starts with
     */
    private SqlFile(Reader reader, String inputStreamLabel) {
        logger.privlog(Level.FINER, "<init>ting SqlFile instance",
                null, 2, FrameworkLogger.class);
        if (reader == null)
            throw new IllegalArgumentException("'reader' may not be null");
        if (inputStreamLabel == null)
            throw new IllegalArgumentException(
                    "'inputStreamLabel' may not be null");

        // Don't try to verify reader.ready() here, since we require it to be
        // reayd to read only in execute(), plus in many caess it's useful for
        // execute() to block.
        this.reader = reader;
        this.inputStreamLabel = inputStreamLabel;
    }

    public void setConnection(Connection jdbcConn) {
        if (jdbcConn == null)
            throw new IllegalArgumentException(
                    "We don't yet support unsetting the JDBC Connection");
        shared.jdbcConn = jdbcConn;
    }

    public Connection getConnection() {
        return shared.jdbcConn;
    }

    public void setContinueOnError(boolean continueOnError) {
        this.continueOnError = continueOnError;
    }

    public void setMaxHistoryLength(int maxHistoryLength) {
        if (executing)
            throw new IllegalStateException(
                "Can't set maxHistoryLength after execute() has been called");
        if (reader == null)
            throw new IllegalStateException(
                "Can't set maxHistoryLength execute() has run");
        this.maxHistoryLength = maxHistoryLength;
    }

    public void addMacros(Map<String, Token> newMacros) {
        shared.macros.putAll(newMacros);
    }

    public void addUserVars(Map<String, String> newUserVars) {
        shared.userVars.putAll(newUserVars);
    }

    public Map<String, String> getUserVars() {
        // Consider whether safer to return a deep copy.  Probably.
        return shared.userVars;
    }

    public Map<String, Token> getMacros() {
        // Consider whether safer to return a deep copy.  Probably.
        return shared.macros;
    }

    /**
     * This sets the instance variable and the corresponding PL variable.
     *
     * @param newEncoding may be null to revert to using defaults again.
     */
    private void setEncoding(String newEncoding)
            throws UnsupportedEncodingException {
        if (newEncoding == null) {
            shared.encoding = null;
            shared.userVars.remove("ENCODING");
            return;
        }
        if (!Charset.isSupported(newEncoding))
            throw new UnsupportedEncodingException(newEncoding);
        shared.userVars.put("*ENCODING", newEncoding);
        shared.encoding = newEncoding;
    }

    // So we can tell how to handle quit and break commands.
    private boolean      recursed;
    private PrintWriter pwQuery;
    private PrintWriter pwDsv;
    private boolean     continueOnError;
    /*
     * This is reset upon each execute() invocation (to true if interactive,
     * false otherwise).
     */
    private SqlFileScanner      scanner;
    private Token               buffer;
    private boolean             preempt;
    private String              lastSqlStatement;
    private boolean             autoClose = true;

    /**
     * Specify whether the supplied or generated input Reader should
     * automatically be closed by the execute() method.
     * <P>
     * execute() will close the Reader by default (i.e. 'autoClose' defaults
     * to true).
     * You may want to set this to false if you want to stop execution with
     * \q or similar, then continue using the Reader or underlying Stream.
     * </P> <P>
     * The caller is always responsible for closing the output object (if any)
     * used by SqlFile.
     * </P>
     */
    public void setAutoClose(boolean autoClose) {
        this.autoClose = autoClose;
    }

    /**
     * Process all the commands from the file or Reader associated with
     * "this" object.
     * SQL commands in the content get executed against the current JDBC
     * data source connection.
     *
     * @throws SQLExceptions thrown by JDBC driver.
     *                       Only possible if in "\c false" mode.
     * @throws SqlToolError  all other errors.
     *               This includes including QuitNow, BreakException,
     *               ContinueException for recursive calls only.
     */
    synchronized public void execute() throws SqlToolError, SQLException {
        if (reader == null)
            throw new IllegalStateException("Can't call execute() "
                    + "more than once for a single SqlFile instance");

        try {
            scanner = new SqlFileScanner(reader);
            scanner.setStdPrintStream(shared.psStd);
            scanner.setRawLeadinPrompt(SqltoolRB.raw_leadin.getString());
            if (interactive) {
                stdprintln(SqltoolRB.SqlFile_banner.getString(revnum));
                scanner.setRawPrompt(rawPrompt);
                scanner.setSqlPrompt(contPrompt);
                scanner.setSqltoolPrompt(primaryPrompt);
                scanner.setInteractive(true);
                if (shared.jdbcConn == null)
                    stdprintln("To connect to a data source, use '\\j "
                        + "urlid' or '\\j account password jdbc:url...'");
                stdprint(primaryPrompt);
            }
            scanpass(scanner);
        } finally {
            try {
                closeQueryOutputStream();
                if (autoClose) closeReader();
            } finally {
                reader = null; // Encourage GC of buffers
            }
        }
    }

    /**
     * Close the reader.
     *
     * The execute method will run this automatically, by default.
     */
    public void closeReader() {
        if (reader == null) {
            return;
        }
        try {
            if (scanner != null) try {
                scanner.yyclose();
            } catch (IOException ioe) {
                errprintln("Failed to close pipes");
            }
            try {
                reader.close();
            } catch (IOException ioe) {
                // Purposefully empty.
                // The reader will usually already be closed at this point.
            }
        } finally {
            reader = null; // Encourage GC of buffers
        }
    }


    /**
     * Returns normalized nesting command String, like "if" or "foreach".
     * If command is not a nesting command, returns null;
     * If there's a proper command String, but the entire PL command is
     * malformatted, throws.
     */
    private String nestingCommand(Token token) throws BadSpecial {
        if (token.type != Token.PL_TYPE) return null;
        // The scanner assures that val is non-null for PL_TYPEs.
        String commandWord = token.val.replaceFirst("\\s.*", "");
        if (!nestingPLCommands.containsKey(commandWord)) return null;
        Pattern pattern = nestingPLCommands.get(commandWord);
        if (pattern.matcher(token.val).matches()) return commandWord;
        throw new BadSpecial(SqltoolRB.pl_malformat.getString());
    }

    synchronized protected void scanpass(TokenSource ts)
                                     throws SqlToolError, SQLException {
        boolean rollbackUncoms = true;
        String nestingCommand;
        Token token = null;

        if (shared.userVars.size() > 0) {
            plMode = true;
        }

        try {
            while (true) try {
                if (preempt) {
                    token = buffer;
                    preempt = false;
                } else {
                    token = ts.yylex();
                    logger.finest("SqlFile got new token:  " + token);
                }
                if (token == null) break;

                nestingCommand = nestingCommand(token);
                if (nestingCommand != null) {
                    if (token.nestedBlock == null) {
                        token.nestedBlock = seekTokenSource(nestingCommand);
                        /* This command (and the same recursive call inside
                         * of the seekTokenSource() method) ensure that all
                         * "blocks" are tokenized immediately as block
                         * commands are encountered, and the blocks are
                         * tokenized in their entirety all the way to the
                         * leaves.
                         */
                    }
                    processBlock(token);
                        /* processBlock recurses through scanpass(),
                         * which processes the nested commands which have
                         * (in all cases) already beeen tokenized.
                         */
                    continue;
                }

                switch (token.type) {
                    case Token.SYNTAX_ERR_TYPE:
                        throw new SqlToolError(SqltoolRB.input_malformat.getString());
                        // Will get here if Scanner can't match input to any
                        // known command type.
                        // An easy way to get here is to start a command with
                        // quotes.
                    case Token.UNTERM_TYPE:
                        throw new SqlToolError(
                                SqltoolRB.input_unterminated.getString(
                                token.val));
                    case Token.RAW_TYPE:
                    case Token.RAWEXEC_TYPE:
                        /*
                         * A real problem in this block is that the Scanner
                         * has already displayed the next prompt at this
                         * point.  We handle this specially within this
                         * block, but if we throw, the handler will not
                         * know that the prompt has to be re-displayed.
                         * I.e., KNOWN ISSUE:  For some errors caught during
                         * raw command execution, interactive users will not
                         * get a prompt to tell them to proceed.
                         */
                        if (token.val == null) token.val = "";
                        /*
                         * Don't have time know to figure out whether it would
                         * ever be useful to send just (non-zero) whitespace
                         * to the DB.  Prohibiting for now.
                         */
                        if (token.val.trim().length() < 1) {
                            throw new SqlToolError(
                                    SqltoolRB.raw_empty.getString());
                        }
                        int receivedType = token.type;
                        token.type = Token.SQL_TYPE;
                        if (setBuf(token) && receivedType == Token.RAW_TYPE
                                && interactive) {
                            stdprintln("");
                            stdprintln(SqltoolRB.raw_movedtobuffer.getString());
                            stdprint(primaryPrompt);
                            // All of these stdprint*'s are to work around a
                            // very complicated issue where the Scanner
                            // has already displayed the next prompt before
                            // we can display our status message.
                        }
                        if (receivedType == Token.RAWEXEC_TYPE) {
                            historize();
                            processSQL();
                        }
                        continue;
                    case Token.MACRO_TYPE:
                        processMacro(token);
                        continue;
                    case Token.PL_TYPE:
                        setBuf(token);
                        historize();
                        processPL(null);
                        continue;
                    case Token.SPECIAL_TYPE:
                        setBuf(token);
                        historize();
                        processSpecial(null);
                        continue;
                    case Token.EDIT_TYPE:
                        // Scanner only returns EDIT_TYPEs in interactive mode
                        processBuffHist(token);
                        continue;
                    case Token.BUFFER_TYPE:
                        token.type = Token.SQL_TYPE;
                        if (setBuf(token)) {
                            stdprintln(
                                    SqltoolRB.input_movedtobuffer.getString());
                        }
                        continue;
                    case Token.SQL_TYPE:
                        if (token.val == null) token.val = "";
                        setBuf(token);
                        historize();
                        processSQL();
                        continue;
                    default:
                        throw new RuntimeException(
                                "Internal assertion failed.  "
                                + "Unexpected token type: "
                                + token.getTypeString());
                }
            } catch (BadSpecial bs) {
                // BadSpecials ALWAYS have non-null getMessage().
                if (token == null) {
                    errprintln(SqltoolRB.errorat.getString(
                            inputStreamLabel, "?", "?", bs.getMessage()));
                } else {
                    errprintln(SqltoolRB.errorat.getString(
                            inputStreamLabel,
                            Integer.toString(token.line),
                            token.reconstitute(),
                            bs.getMessage(), bs.getMessage()));
                }
                Throwable cause = bs.getCause();
                if (cause != null) {
                    errprintln(SqltoolRB.causereport.getString(
                            cause.toString()));

                }

                if (!continueOnError) {
                    throw new SqlToolError(bs);
                }
            } catch (SQLException se) {
                //se.printStackTrace();
                errprintln("SQL " + SqltoolRB.errorat.getString(
                        inputStreamLabel,
                        ((token == null) ? "?"
                                         : Integer.toString(token.line)),
                        lastSqlStatement,
                        se.getMessage()));
                // It's possible that we could have
                // SQLException.getMessage() == null, but if so, I think
                // it reasonable to show "null".  That's a DB inadequacy.

                if (!continueOnError) {
                    throw se;
                }
            } catch (BreakException be) {
                String msg = be.getMessage();

                if (recursed) {
                    rollbackUncoms = false;
                    // Recursion level will exit by rethrowing the BE.
                    // We set rollbackUncoms to false because only the
                    // top level should detect break errors and
                    // possibly roll back.
                } else if (msg == null || msg.equals("file")) {
                    break;
                } else {
                    errprintln(SqltoolRB.break_unsatisfied.getString(msg));
                }

                if (recursed ||!continueOnError) {
                    throw be;
                }
            } catch (ContinueException ce) {
                String msg = ce.getMessage();

                if (recursed) {
                    rollbackUncoms = false;
                } else {
                    errprintln(SqltoolRB.continue_unsatisfied.getString(msg));
                }

                if (recursed ||!continueOnError) {
                    throw ce;
                }
            } catch (QuitNow qn) {
                throw qn;
            } catch (SqlToolError ste) {
                StringBuffer sb = new StringBuffer(SqltoolRB.errorat.getString(
                    /* WARNING:  I have removed an extra LS appended to
                     * non-null ste.getMessages() below because I believe that
                     * it is unnecessary (and causes inconsistent blank lines
                     * to be written).
                     * If I am wrong and this is needed for Scanner display or
                     * something, restore it.
                     */
                    ((token == null)
                            ? (new String[] {
                                inputStreamLabel, "?", "?",
                                ((ste.getMessage() == null)
                                        ? "" : ste.getMessage())
                              })
                            : (new String[] {
                                inputStreamLabel, Integer.toString(token.line),
                                ((token.val == null) ? "" : token.reconstitute()),
                                ((ste.getMessage() == null)
                                        ? "" : ste.getMessage())
                              }))
                ));
                Throwable cause = ste.getCause();
                errprintln((cause == null) ? sb.toString()
                        : SqltoolRB.causereport.getString(cause.toString()));
                if (!continueOnError) {
                    throw ste;
                }
            }

            rollbackUncoms = false;
            // Exiting gracefully, so don't roll back.
        } catch (IOException ioe) {
            throw new SqlToolError(
                    SqltoolRB.primaryinput_accessfail.getString(), ioe);
        } catch (QuitNow qn) {
            if (recursed) {
                throw qn;
                // Will rollback if conditions otherwise require.
                // Otherwise top level will decide based upon qn.getMessage().
            }
            rollbackUncoms = (qn.getMessage() != null);

            if (rollbackUncoms) {
                errprintln(SqltoolRB.aborting.getString(qn.getMessage()));
                throw new SqlToolError(qn.getMessage());
            }

            return;
        } finally {
            if (fetchingVar != null) {
                errprintln(SqltoolRB.plvar_set_incomplete.getString(
                        fetchingVar));
                rollbackUncoms = true;
            }
            if (shared.jdbcConn != null) {
                if (shared.jdbcConn.getAutoCommit())
                    shared.possiblyUncommitteds = false;
                if (rollbackUncoms && shared.possiblyUncommitteds) {
                    errprintln(SqltoolRB.rollingback.getString());
                    shared.jdbcConn.rollback();
                    shared.possiblyUncommitteds = false;
                }
            }
        }
    }

    /**
     * Utility nested Exception class for internal use only.
     *
     * Do not instantiate with null message.
     */
    private static class BadSpecial extends AppendableException {
        static final long serialVersionUID = 7162440064026570590L;

        BadSpecial(String s) {
            super(s);
            if (s == null)
                throw new RuntimeException(
                        "Must construct BadSpecials with non-null message");
        }
        BadSpecial(String s, Throwable t) {
            super(s, t);
            if (s == null)
                throw new RuntimeException(
                        "Must construct BadSpecials with non-null message");
        }
    }

    /**
     * Utility nested Exception class for internal use.
     * This must extend SqlToolError because it has to percolate up from
     * recursions of SqlTool.execute(), yet SqlTool.execute() is public.
     * Therefore, external users have no reason to specifically handle
     * QuitNow.
     */
    private class QuitNow extends SqlToolError {
        static final long serialVersionUID = 1811094258670900488L;

        public QuitNow(String s) {
            super(s);
        }

        public QuitNow() {
            super();
        }
    }

    /**
     * Utility nested Exception class for internal use.
     * This must extend SqlToolError because it has to percolate up from
     * recursions of SqlTool.execute(), yet SqlTool.execute() is public.
     * Therefore, external users have no reason to specifically handle
     * BreakException.
     */
    private class BreakException extends SqlToolError {
        static final long serialVersionUID = 351150072817675994L;

        public BreakException() {
            super();
        }

        public BreakException(String s) {
            super(s);
        }
    }

    /**
     * Utility nested Exception class for internal use.
     * This must extend SqlToolError because it has to percolate up from
     * recursions of SqlTool.execute(), yet SqlTool.execute() is public.
     * Therefore, external users have no reason to specifically handle
     * ContinueException.
     */
    private class ContinueException extends SqlToolError {
        static final long serialVersionUID = 5064604160827106014L;

        public ContinueException() {
            super();
        }

        public ContinueException(String s) {
            super(s);
        }
    }

    /**
     * Utility nested Exception class for internal use only.
     */
    private class BadSubst extends Exception {
        static final long serialVersionUID = 7325933736897253269L;

        BadSubst(String s) {
            super(s);
        }
    }

    /**
     * Utility nested Exception class for internal use only.
     */
    private class RowError extends AppendableException {
        static final long serialVersionUID = 754346434606022750L;

        RowError(String s) {
            super(s);
        }

        /* Unused so far
        RowError(Throwable t) {
            this(null, t);
        }
        */

        RowError(String s, Throwable t) {
            super(s, t);
        }
    }

    /**
     * Process a Buffer/History Command.
     *
     * Due to the nature of the goal here, we don't trim() "other" like
     * we do for other kinds of commands.
     *
     * @param inString Complete command, less the leading ':' character.
     * @throws SQLException  thrown by JDBC driver.
     * @throws BadSpecial    special-command-specific errors.
     * @throws SqlToolError  all other errors.
     */
    private void processBuffHist(Token token)
    throws BadSpecial, SQLException, SqlToolError {
        if (token.val.length() < 1) {
            throw new BadSpecial(SqltoolRB.bufhist_unspecified.getString());
        }

        // First handle the simple cases where user may not specify a
        // command number.
        char commandChar = token.val.charAt(0);
        String other       = token.val.substring(1);
        if (other.trim().length() == 0) {
            other = null;
        }
        switch (commandChar) {
            case 'l' :
            case 'b' :
                enforce1charBH(other, 'l');
                if (buffer == null) {
                    stdprintln(nobufferYetString);
                } else {
                    stdprintln(SqltoolRB.editbuffer_contents.getString(
                            buffer.reconstitute()));
                }

                return;

            case 'h' :
                enforce1charBH(other, 'h');
                showHistory();

                return;

            case '?' :
                stdprintln(SqltoolRB.buffer_help.getString());

                return;
        }

        Integer histNum = null;
        Matcher hm = slashHistoryPattern.matcher(token.val);
        if (hm.matches()) {
            histNum = historySearch(hm.group(1));
            if (histNum == null) {
                stdprintln(SqltoolRB.substitution_nomatch.getString());
                return;
            }
        } else {
            hm = historyPattern.matcher(token.val);
            if (!hm.matches()) {
                throw new BadSpecial(SqltoolRB.edit_malformat.getString());
                // Empirically, I find that this pattern always captures two
                // groups.  Unfortunately, there's no way to guarantee that :( .
            }
            histNum = ((hm.group(1) == null || hm.group(1).length() < 1)
                    ? null : Integer.valueOf(hm.group(1)));
        }
        if (hm.groupCount() != 2) {
            throw new BadSpecial(SqltoolRB.edit_malformat.getString());
            // Empirically, I find that this pattern always captures two
            // groups.  Unfortunately, there's no way to guarantee that :( .
        }
        commandChar = ((hm.group(2) == null || hm.group(2).length() < 1)
                ? '\0' : hm.group(2).charAt(0));
        other = ((commandChar == '\0') ? null : hm.group(2).substring(1));
        if (other != null && other.length() < 1) other = null;
        Token targetCommand = ((histNum == null)
                ? null : commandFromHistory(histNum.intValue()));
        // Every command below depends upon buffer content.

        switch (commandChar) {
            case '\0' // Special token set above.  Just history recall.
                setBuf(targetCommand);
                stdprintln(SqltoolRB.buffer_restored.getString(
                        buffer.reconstitute()));
                return;

            case ';' :
                enforce1charBH(other, ';');

                if (targetCommand != null) setBuf(targetCommand);
                if (buffer == null) throw new BadSpecial(
                        SqltoolRB.nobuffer_yet.getString());
                stdprintln(SqltoolRB.buffer_executing.getString(
                        buffer.reconstitute()));
                preempt = true;
                return;

            case 'a' :
                if (targetCommand == null) targetCommand = buffer;
                if (targetCommand == null) throw new BadSpecial(
                        SqltoolRB.nobuffer_yet.getString());
                boolean doExec = false;

                if (other != null) {
                    if (other.trim().charAt(other.trim().length() - 1) == ';') {
                        other = other.substring(0, other.lastIndexOf(';'));
                        if (other.trim().length() < 1)
                            throw new BadSpecial(
                                    SqltoolRB.append_empty.getString());
                        doExec = true;
                    }
                }
                Token newToken = new Token(targetCommand.type,
                        targetCommand.val, targetCommand.line);
                if (other != null) newToken.val += other;
                setBuf(newToken);
                if (doExec) {
                    stdprintln(SqltoolRB.buffer_executing.getString(
                            buffer.reconstitute()));
                    preempt = true;
                    return;
                }

                if (interactive) scanner.setMagicPrefix(
                        newToken.reconstitute());

                switch (newToken.type) {
                    case Token.SQL_TYPE:
                        scanner.setRequestedState(SqlFileScanner.SQL);
                        break;
                    case Token.SPECIAL_TYPE:
                        scanner.setRequestedState(SqlFileScanner.SPECIAL);
                        break;
                    case Token.PL_TYPE:
                        scanner.setRequestedState(SqlFileScanner.PL);
                        break;
                    default:
                        throw new RuntimeException(
                            "Internal assertion failed.  "
                            + "Appending to unexpected type: "
                            + newToken.getTypeString());
                }
                scanner.setCommandBuffer(newToken.val);

                return;

            case 'w' :
                if (targetCommand == null) targetCommand = buffer;
                if (targetCommand == null) throw new BadSpecial(
                        SqltoolRB.nobuffer_yet.getString());
                if (other == null) {
                    throw new BadSpecial(SqltoolRB.destfile_demand.getString());
                }
                String targetFile = dereference(other.trim(), false);
                // Dereference and trim the target file name
                // This is the only case where we dereference a : command.

                PrintWriter pw = null;
                try {
                    pw = new PrintWriter(
                            new OutputStreamWriter(
                            new FileOutputStream(targetFile, true),
                            (shared.encoding == null)
                            ? DEFAULT_FILE_ENCODING : shared.encoding)
                            // Appendmode so can append to an SQL script.
                    );

                    pw.println(targetCommand.reconstitute(true));
                    pw.flush();
                } catch (Exception e) {
                    throw new BadSpecial(SqltoolRB.file_appendfail.getString(
                            targetFile), e);
                } finally {
                    if (pw != null) {
                        try {
                            pw.close();
                        } finally {
                            pw = null; // Encourage GC of buffers
                        }
                    }
                }

                return;

            case 's' :
                boolean modeExecute = false;
                boolean modeGlobal = false;
                if (targetCommand == null) targetCommand = buffer;
                if (targetCommand == null) throw new BadSpecial(
                        SqltoolRB.nobuffer_yet.getString());

                try {
                    if (other == null || other.length() < 3) {
                        throw new BadSubst(
                                SqltoolRB.substitution_malformat.getString());
                    }
                    Matcher m = substitutionPattern.matcher(other);
                    if (!m.matches()) {
                        throw new BadSubst(
                                SqltoolRB.substitution_malformat.getString());
                    }

                    // Note that this pattern does not include the leading :.
                    if (m.groupCount() < 3 || m.groupCount() > 4) {
                        throw new RuntimeException(
                                "Internal assertion failed.  "
                                + "Matched substitution "
                                + "pattern, but captured "
                                + m.groupCount() + " groups");
                    }
                    String optionGroup = (
                            (m.groupCount() > 3 && m.group(4) != null)
                            ? (new String(m.group(4))) : null);

                    if (optionGroup != null) {
                        if (optionGroup.indexOf(';') > -1) {
                            modeExecute = true;
                            optionGroup = optionGroup.replaceFirst(";", "");
                        }
                        if (optionGroup.indexOf('g') > -1) {
                            modeGlobal = true;
                            optionGroup = optionGroup.replaceFirst("g", "");
                        }
                    }

                    Matcher bufferMatcher = Pattern.compile("(?s"
                            + ((optionGroup == null) ? "" : optionGroup)
                            + ')' + m.group(2)).matcher(targetCommand.val);
                    Token newBuffer = new Token(targetCommand.type,
                            (modeGlobal
                                ? bufferMatcher.replaceAll(m.group(3))
                                : bufferMatcher.replaceFirst(m.group(3))),
                                targetCommand.line);
                    if (newBuffer.val.equals(targetCommand.val)) {
                        stdprintln(SqltoolRB.substitution_nomatch.getString());
                        return;
                    }

                    setBuf(newBuffer);
                    stdprintln(modeExecute
                            ? SqltoolRB.buffer_executing.getString(
                                    buffer.reconstitute())
                            : SqltoolRB.editbuffer_contents.getString(
                                    buffer.reconstitute())
                    );
                } catch (PatternSyntaxException pse) {
                    throw new BadSpecial(
                            SqltoolRB.substitution_syntax.getString(), pse);
                } catch (BadSubst badswitch) {
                    throw new BadSpecial(
                            SqltoolRB.substitution_syntax.getString());
                }
                if (modeExecute) preempt = true;

                return;
        }

        throw new BadSpecial(SqltoolRB.buffer_unknown.getString(
                Character.toString(commandChar)));
    }

    private boolean doPrepare;
    private String  prepareVar;
    private String  dsvColDelim;
    private String  dsvColSplitter;
    private String  dsvSkipPrefix;
    private String  dsvRowDelim;
    private String  dsvRowSplitter;
    private String  dsvSkipCols;
    private boolean dsvTrimAll;
    private static String  DSV_X_SYNTAX_MSG;
    private static String  DSV_M_SYNTAX_MSG;
    private static String  nobufferYetString;

    private void enforce1charSpecial(String tokenString, char command)
            throws BadSpecial {
        if (tokenString.length() != 1) {
            throw new BadSpecial(SqltoolRB.special_extrachars.getString(
                     Character.toString(command), tokenString.substring(1))); } }
    private void enforce1charBH(String tokenString, char command)
            throws BadSpecial {
        if (tokenString != null) {
            throw new BadSpecial(SqltoolRB.buffer_extrachars.getString(
                    Character.toString(command), tokenString));
        }
    }

    /**
     * Process a Special Command.
     *
     * @param inString TRIMMED, no-null command (without leading \),
     *                 or null to operate on buffer.
     * @throws SQLException thrown by JDBC driver.
     * @throws BadSpecial special-command-specific errors.
     * @throws SqlToolError all other errors, plus QuitNow,
     *                      BreakException, ContinueException.
     */
    private void processSpecial(String inString)
    throws BadSpecial, QuitNow, SQLException, SqlToolError {
        String string = (inString == null) ? buffer.val : inString;
        if (string.length() < 1) {
            throw new BadSpecial(SqltoolRB.special_unspecified.getString());
        }
        Matcher m = specialPattern.matcher(
                plMode ? dereference(string, false) : string);
        if (!m.matches()) {
            throw new BadSpecial(SqltoolRB.special_malformat.getString());
            // I think it's impossible to get here, since the pattern is
            // so liberal.
        }
        if (m.groupCount() < 1 || m.groupCount() > 2) {
            // Failed assertion
            throw new RuntimeException(
                    "Internal assertion failed.  Pattern matched, yet captured "
                    + m.groupCount() + " groups");
        }

        String arg1 = m.group(1);
        String other = ((m.groupCount() > 1) ? m.group(2) : null);

        switch (arg1.charAt(0)) {
            case 'q' :
                enforce1charSpecial(arg1, 'q');
                if (other != null) {
                    throw new QuitNow(other);
                }

                throw new QuitNow();
            case 'H' :
                enforce1charSpecial(arg1, 'H');
                htmlMode = !htmlMode;

                stdprintln(SqltoolRB.html_mode.getString(
                        Boolean.toString(htmlMode)));

                return;

            case 'm' :
                if (arg1.equals("m?") ||
                        (arg1.equals("m") && other != null
                                 && other.equals("?"))) {
                    stdprintln(DSV_OPTIONS_TEXT + LS + DSV_M_SYNTAX_MSG);
                    return;
                }
                if (arg1.length() != 1 || other == null) {
                    throw new BadSpecial(DSV_M_SYNTAX_MSG);
                }
                boolean noComments = other.charAt(other.length() - 1) == '*';
                String skipPrefix = null;

                if (noComments) {
                    other = other.substring(0, other.length()-1).trim();
                    if (other.length() < 1) {
                        throw new BadSpecial(DSV_M_SYNTAX_MSG);
                    }
                } else {
                    skipPrefix = dsvSkipPrefix;
                }
                int colonIndex = other.indexOf(" :");
                if (colonIndex > -1 && colonIndex < other.length() - 2) {
                    skipPrefix = other.substring(colonIndex + 2);
                    other = other.substring(0, colonIndex).trim();
                }

                importDsv(other, skipPrefix);

                return;

            case 'x' :
                requireConnection();
                if (arg1.equals("x?") ||
                        (arg1.equals("x") && other != null
                                 && other.equals("?"))) {
                    stdprintln(DSV_OPTIONS_TEXT + LS + DSV_X_SYNTAX_MSG);
                    return;
                }
                try {
                    if (arg1.length() != 1 || other == null) {
                        throw new BadSpecial(DSV_X_SYNTAX_MSG);
                    }

                    String tableName = ((other.indexOf(' ') > 0) ? null
                                                                 : other);

                    if (dsvTargetFile == null && tableName == null) {
                        throw new BadSpecial(
                                SqltoolRB.dsv_targetfile_demand.getString());
                    }
                    File dsvFile = new File((dsvTargetFile == null)
                                            ? (tableName + ".dsv")
                                            : dsvTargetFile);

                    pwDsv = new PrintWriter(new OutputStreamWriter(
                            new FileOutputStream(dsvFile),
                            (shared.encoding == null)
                            ? DEFAULT_FILE_ENCODING : shared.encoding));

                    ResultSet rs = shared.jdbcConn.createStatement()
                            .executeQuery((tableName == null) ? other
                                                : ("SELECT * FROM "
                                                   + tableName));
                    try {
                        List<Integer> colList = new ArrayList<Integer>();
                        int[] incCols = null;
                        if (dsvSkipCols != null) {
                            Set<String> skipCols = new HashSet<String>();
                            for (String s : dsvSkipCols.split(dsvColDelim, -1)) {
                            // Don't know if better to use dsvColDelim or
                            // dsvColSplitter.  Going with former, since the
                            // latter should not need to be set for eXporting
                            // (only importing).
                                skipCols.add(s.trim().toLowerCase());
                            }
                            ResultSetMetaData rsmd = rs.getMetaData();
                            for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                                if (!skipCols.remove(rsmd.getColumnName(i)
                                        .toLowerCase())) {
                                    colList.add(Integer.valueOf(i));
                                }
                            }
                            if (colList.size() < 1) {
                                throw new BadSpecial(
                                        SqltoolRB.dsv_nocolsleft.getString(
                                        dsvSkipCols));
                            }
                            if (skipCols.size() > 0) {
                                throw new BadSpecial(
                                        SqltoolRB.dsv_skipcols_missing.getString(
                                        skipCols.toString()));
                            }
                            incCols = new int[colList.size()];
                            for (int i = 0; i < incCols.length; i++) {
                                incCols[i] = colList.get(i).intValue();
                            }
                        }
                        displayResultSet(null, rs, incCols, null);
                    } finally {
                        rs.close();
                    }
                    pwDsv.flush();
                    stdprintln(SqltoolRB.file_wrotechars.getString(
                            Long.toString(dsvFile.length()),
                            dsvFile.toString()));
                } catch (FileNotFoundException e) {
                    throw new BadSpecial(SqltoolRB.file_writefail.getString(
                            other), e);
                } catch (UnsupportedEncodingException e) {
                    throw new BadSpecial(SqltoolRB.file_writefail.getString(
                            other), e);
                } finally {
                    // Reset all state changes
                    if (pwDsv != null) {
                        try {
                            pwDsv.close();
                        } finally {
                            pwDsv = null; // Encourage GC of buffers
                        }
                    }
                }

                return;

            case 'd' :
                requireConnection();
                if (arg1.equals("d?") ||
                        (arg1.equals("d") && other != null
                                 && other.equals("?"))) {
                    stdprintln(D_OPTIONS_TEXT);
                    return;
                }
                if (arg1.length() == 2) {
                    listTables(arg1.charAt(1), other);

                    return;
                }

                if (arg1.length() == 1 && other != null) try {
                    int space = other.indexOf(' ');

                    if (space < 0) {
                        describe(other, null);
                    } else {
                        describe(other.substring(0, space),
                                 other.substring(space + 1).trim());
                    }

                    return;
                } catch (SQLException se) {
                    throw new BadSpecial(
                            SqltoolRB.metadata_fetch_fail.getString(), se);
                }

                throw new BadSpecial(SqltoolRB.special_d_like.getString());
            case 'o' :
                enforce1charSpecial(arg1, 'o');
                if (other == null) {
                    if (pwQuery == null) {
                        throw new BadSpecial(
                                SqltoolRB.outputfile_nonetoclose.getString());
                    }

                    closeQueryOutputStream();

                    return;
                }

                if (pwQuery != null) {
                    stdprintln(SqltoolRB.outputfile_reopening.getString());
                    closeQueryOutputStream();
                }

                try {
                    pwQuery = new PrintWriter(new OutputStreamWriter(
                            new FileOutputStream(other, true),
                            (shared.encoding == null)
                            ? DEFAULT_FILE_ENCODING : shared.encoding));

                    /* Opening in append mode, so it's possible that we will
                     * be adding superfluous <HTML> and <BODY> tags.
                     * I think that browsers can handle that */
                    pwQuery.println((htmlMode
                            ? ("<HTML>" + LS + "<!--")
                            : "#") + " " + (new java.util.Date()) + ".  "
                                    + SqltoolRB.outputfile_header.getString(
                                    getClass().getName())
                                    + (htmlMode ? (" -->" + LS + LS + "<BODY>")
                                                : LS));
                    pwQuery.flush();
                } catch (Exception e) {
                    throw new BadSpecial(SqltoolRB.file_writefail.getString(
                            other), e);
                }

                return;

            case 'i' :
                enforce1charSpecial(arg1, 'i');
                if (other == null) {
                    throw new BadSpecial(
                            SqltoolRB.sqlfile_name_demand.getString());
                }

                try {
                    new SqlFile(this, new File(other)).execute();
                } catch (ContinueException ce) {
                    throw ce;
                } catch (BreakException be) {
                    String beMessage = be.getMessage();

                    // Handle "file" and plain breaks (by doing nothing)
                    if (beMessage != null &&!beMessage.equals("file")) {
                        throw be;
                    }
                } catch (QuitNow qn) {
                    throw qn;
                } catch (Exception e) {
                    throw new BadSpecial(
                            SqltoolRB.sqlfile_execute_fail.getString(other), e);
                }

                return;

            case 'p' :
                enforce1charSpecial(arg1, 'p');
                if (other == null) {
                    stdprintln(true);
                } else {
                    stdprintln(other, true);
                }

                return;

            case 'l' :
                if ((arg1.equals("l?") && other == null)
                        || (arg1.equals("l") && other != null
                                && other.equals("?"))) {
                    stdprintln(SqltoolRB.log_syntax.getString());
                } else {
                    enforce1charSpecial(arg1, 'l');
                    Matcher logMatcher = ((other == null) ? null
                            : logPattern.matcher(other.trim()));
                    if (logMatcher == null || (!logMatcher.matches()))
                        throw new BadSpecial(
                                SqltoolRB.log_syntax_error.getString());
                    String levelString = logMatcher.group(1);
                    Level level = null;
                    if (levelString.equalsIgnoreCase("FINER"))
                        level = Level.FINER;
                    else if (levelString.equalsIgnoreCase("WARNING"))
                        level = Level.WARNING;
                    else if (levelString.equalsIgnoreCase("SEVERE"))
                        level = Level.SEVERE;
                    else if (levelString.equalsIgnoreCase("INFO"))
                        level = Level.INFO;
                    else if (levelString.equalsIgnoreCase("FINEST"))
                        level = Level.FINEST;
                    if (level == null)
                        throw new RuntimeException(
                                "Internal assertion failed.  "
                                + " Unexpected Level string: " + levelString);
                    logger.enduserlog(level, logMatcher.group(2));
                }

                return;

            case 'a' :
                requireConnection();
                enforce1charSpecial(arg1, 'a');
                if (other != null) {
                    shared.jdbcConn.setAutoCommit(
                        Boolean.parseBoolean(other));
                    shared.possiblyUncommitteds = false;
                }

                stdprintln(SqltoolRB.a_setting.getString(
                        Boolean.toString(shared.jdbcConn.getAutoCommit())));

                return; case 'j' : try {
                enforce1charSpecial(arg1, 'j');
                String urlid = null;
                String acct = null;
                String pwd = null;
                String url = null;
                boolean goalAutoCommit = false;
                String[] tokens = (other == null)
                        ? (new String[0]) : other.split("\\s+", 3);
                switch (tokens.length) {
                    case 0:
                        break;
                    case 1:
                        urlid = tokens[0];
                        break;
                    case 2:
                        acct = tokens[0];
                        pwd = ""// default password to ""
                        url = tokens[1];
                        break;
                    case 3:
                        acct = tokens[0];
                        pwd = tokens[1];
                        url = tokens[2];
                        break;
                }
                if (tokens.length > 0) {
                    // Close current connection
                    if (shared.jdbcConn != null) try {
                        goalAutoCommit = shared.jdbcConn.getAutoCommit();
                        shared.jdbcConn.close();
                        shared.possiblyUncommitteds = false;
                        shared.jdbcConn = null;
                        stdprintln(SqltoolRB.disconnect_success.getString());
                    } catch (SQLException se) {
                        throw new BadSpecial(
                                SqltoolRB.disconnect_failure.getString(), se);
                    }
                }
                if (urlid != null || acct != null) try {
                    if (urlid != null) {
                        shared.jdbcConn = new RCData(new File(
                            SqlTool.DEFAULT_RCFILE), urlid).getConnection();
                    } else if (acct != null) {
                        shared.jdbcConn =
                                DriverManager.getConnection(url, acct, pwd);
                    }
                    shared.possiblyUncommitteds = false;
                    shared.jdbcConn.setAutoCommit(goalAutoCommit);
                } catch (Exception e) {
                    throw new BadSpecial("Failed to connect", e);
                }
                displayConnBanner();
            } catch (Throwable t) {
                t.printStackTrace();
                return;
            }
                return;
            case 'v' :
                requireConnection();
                enforce1charSpecial(arg1, 'v');
                if (other != null) {
                    if (integerPattern.matcher(other).matches()) {
                        shared.jdbcConn.setTransactionIsolation(
                                Integer.parseInt(other));
                    } else {
                        RCData.setTI(shared.jdbcConn, other);
                    }
                }

                stdprintln(SqltoolRB.transiso_report.getString(
                        (shared.jdbcConn.isReadOnly() ? "R/O " : "R/W "),
                        RCData.tiToString(
                                shared.jdbcConn.getTransactionIsolation())));

                return;
            case '=' :
                requireConnection();
                enforce1charSpecial(arg1, '=');
                shared.jdbcConn.commit();
                shared.possiblyUncommitteds = false;
                stdprintln(SqltoolRB.committed.getString());

                return;

            case 'b' :
                if (arg1.length() == 1) {
                    if (other != null) {
                        throw new BadSpecial(
                                SqltoolRB.special_b_malformat.getString());
                    }
                    fetchBinary = true;

                    return;
                }

                if (arg1.charAt(1) == 'p') {
                    if (other != null) {
                        throw new BadSpecial(
                                SqltoolRB.special_b_malformat.getString());
                    }
                    doPrepare = true;

                    return;
                }

                if ((arg1.charAt(1) != 'd' && arg1.charAt(1) != 'l')
                        || other == null) {
                    throw new BadSpecial(
                            SqltoolRB.special_b_malformat.getString());
                }

                File otherFile = new File(other);

                try {
                    if (arg1.charAt(1) == 'd') {
                        dump(otherFile);
                    } else {
                        binBuffer = SqlFile.loadBinary(otherFile);
                        stdprintln(SqltoolRB.binary_loadedbytesinto.getString(
                                binBuffer.length));
                    }
                } catch (BadSpecial bs) {
                    throw bs;
                } catch (IOException ioe) {
                    throw new BadSpecial(SqltoolRB.binary_filefail.getString(
                            other), ioe);
                }

                return;

            case 't' :
                enforce1charSpecial(arg1, '=');
                if (other != null) {
                    // But remember that we have to abort on some I/O errors.
                    reportTimes = Boolean.parseBoolean(other);
                }

                stdprintln(SqltoolRB.exectime_reporting.getString(
                        Boolean.toString(reportTimes)));
                return;

            case '*' :
            case 'c' :
                enforce1charSpecial(arg1, '=');
                if (other != null) {
                    // But remember that we have to abort on some I/O errors.
                    continueOnError = Boolean.parseBoolean(other);
                }

                stdprintln(SqltoolRB.c_setting.getString(
                        Boolean.toString(continueOnError)));

                return;

            case '?' :
                stdprintln(SqltoolRB.special_help.getString());

                return;

            case '!' :
                /* N.b. This DOES NOT HANDLE UNIX shell wildcards, since there
                 * is no UNIX shell involved.
                 * Doesn't make sense to incur overhead of a shell without
                 * stdin capability.
                 * Could pipe System.in to the forked process, but that's
                 * probably not worth the effort due to Java's terrible
                 * and inescapable System.in buffering.  I.e., the forked
                 * program or shell wouldn't get stdin until user hits Enter.
                 *
                 * I'd like to execute the user's default shell if they
                 * ran "\!" with no argument, but (a) there is no portable
                 * way to determine the user's default or login shell; and
                 * (b) shell is useless without stdin ability.
                 */

                InputStream stream;
                byte[]      ba         = new byte[1024];
                String      extCommand = ((arg1.length() == 1)
                        ? "" : arg1.substring(1))
                    + ((arg1.length() > 1 && other != null)
                       ? " " : "") + ((other == null) ? "" : other);
                if (extCommand.trim().length() < 1)
                    throw new BadSpecial(SqltoolRB.bang_incomplete.getString());

                Process proc = null;
                try {
                    Runtime runtime = Runtime.getRuntime();
                    proc = ((wincmdPattern == null)
                            ? runtime.exec(extCommand)
                            : runtime.exec(genWinArgs(extCommand))
                    );

                    proc.getOutputStream().close();

                    int i;

                    stream = proc.getInputStream();

                    while ((i = stream.read(ba)) > 0) {
                        stdprint(new String(ba, 0, i));
                    }

                    stream.close();

                    stream = proc.getErrorStream();

                    String s;
                    while ((i = stream.read(ba)) > 0) {
                        s = new String(ba, 0, i);
                        if (s.endsWith(LS)) {
                            // This block just prevents logging of
                            // double-line-breaks.
                            if (s.length() == LS.length()) continue;
                            s = s.substring(0, s.length() - LS.length());
                        }
                        logger.severe(s);
                    }

                    stream.close();
                    stream = null// Encourage buffer GC

                    if (proc.waitFor() != 0) {
                        throw new BadSpecial(
                                SqltoolRB.bang_command_fail.getString(
                                extCommand));
                    }
                } catch (BadSpecial bs) {
                    throw bs;
                } catch (Exception e) {
                    throw new BadSpecial(SqltoolRB.bang_command_fail.getString(
                            extCommand), e);
                } finally {
                    if (proc != null) {
                        proc.destroy();
                    }
                }

                return;
        }

        throw new BadSpecial(SqltoolRB.special_unknown.getString(
                Character.toString(arg1.charAt(0))));
    }

    private static final char[] nonVarChars = {
        ' ', '\t', '=', '}', '\n', '\r', '\f'
    };

    /**
     * Returns index specifying 1 past end of a variable name.
     *
     * @param inString String containing a variable name
     * @param startIndex Index within inString where the variable name begins
     * @return Index within inString, 1 past end of the variable name
     */
    static int pastName(String inString, int startIndex) {
        String workString = inString.substring(startIndex);
        int    e          = inString.length()// Index 1 past end of var name.
        int    nonVarIndex;

        for (char nonVarChar : nonVarChars) {
            nonVarIndex = workString.indexOf(nonVarChar);

            if (nonVarIndex > -1 && nonVarIndex < e) {
                e = nonVarIndex;
            }
        }

        return startIndex + e;
    }

    /**
     * Deference *{} PL variables and ${} System Property variables.
     *
     * @throws SqlToolError
     */
    private String dereference(String inString,
                               boolean permitAlias) throws SqlToolError {
        if (inString.length() < 1) return inString;

        /* TODO:  Rewrite using java.util.regex. */
        String       varName, varValue;
        StringBuffer expandBuffer = new StringBuffer(inString);
        int          b, e;    // begin and end of name.  end really 1 PAST name
        int iterations;

        if (permitAlias && inString.trim().charAt(0) == '/') {
            int slashIndex = inString.indexOf('/');

            e = SqlFile.pastName(inString.substring(slashIndex + 1), 0);

            // In this case, e is the exact length of the var name.
            if (e < 1) {
                throw new SqlToolError(SqltoolRB.plalias_malformat.getString());
            }

            varName  = inString.substring(slashIndex + 1, slashIndex + 1 + e);
            varValue = shared.userVars.get(varName);

            if (varValue == null) {
                throw new SqlToolError(
                        SqltoolRB.plvar_undefined.getString(varName));
            }

            expandBuffer.replace(slashIndex, slashIndex + 1 + e,
                                 shared.userVars.get(varName));
        }

        String s;
        boolean permitUnset;
        // Permit unset with:     ${:varname}
        // Prohibit unset with :  ${varnam}

        iterations = 0;
        while (true) {
            s = expandBuffer.toString();
            b = s.indexOf("${");

            if (b < 0) {
                // No more unexpanded variable uses
                break;
            }

            e = s.indexOf('}', b + 2);

            if (e == b + 2) {
                throw new SqlToolError(SqltoolRB.sysprop_empty.getString());
            }

            if (e < 0) {
                throw new SqlToolError(
                        SqltoolRB.sysprop_unterminated.getString());
            }

            permitUnset = (s.charAt(b + 2) == ':');

            varName = s.substring(b + (permitUnset ? 3 : 2), e);
            if (iterations++ > 10000)
                throw new
                    SqlToolError(SqltoolRB.var_infinite.getString(varName));

            varValue = System.getProperty(varName);
            if (varValue == null) {
                if (permitUnset) {
                    varValue = "";
                } else {
                    throw new SqlToolError(
                            SqltoolRB.sysprop_undefined.getString(varName));
                }
            }

            expandBuffer.replace(b, e + 1, varValue);
        }

        iterations = 0;
        while (true) {
            s = expandBuffer.toString();
            b = s.indexOf("*{");

            if (b < 0) {
                // No more unexpanded variable uses
                break;
            }

            e = s.indexOf('}', b + 2);

            if (e == b + 2) {
                throw new SqlToolError(SqltoolRB.plvar_nameempty.getString());
            }

            if (e < 0) {
                throw new SqlToolError(
                        SqltoolRB.plvar_unterminated.getString());
            }

            permitUnset = (s.charAt(b + 2) == ':');

            varName = s.substring(b + (permitUnset ? 3 : 2), e);
            if (iterations++ > 100000)
                throw new SqlToolError(
                        SqltoolRB.var_infinite.getString(varName));
            // TODO:  Use a smarter algorithm to handle (or prohibit)
            // recursion without this clumsy detection tactic.

            varValue = shared.userVars.get(varName);
            if (varValue == null) {
                if (permitUnset) {
                    varValue = "";
                } else {
                    throw new SqlToolError(
                            SqltoolRB.plvar_undefined.getString(varName));
                }
            }

            expandBuffer.replace(b, e + 1, varValue);
        }

        return expandBuffer.toString();
    }

    private boolean plMode;

    //  PL variable name currently awaiting query output.
    private String  fetchingVar;
    private boolean silentFetch;
    private boolean fetchBinary;

    /**
     * Process a block PL command like "if" of "foreach".
     */
    private void processBlock(Token token) throws BadSpecial, SqlToolError {
        Matcher m = plPattern.matcher(dereference(token.val, false));
        if (!m.matches()) {
            throw new BadSpecial(SqltoolRB.pl_malformat.getString());
            // I think it's impossible to get here, since the pattern is
            // so liberal.
        }
        if (m.groupCount() < 1 || m.group(1) == null) {
            plMode = true;
            stdprintln(SqltoolRB.pl_expansionmode.getString("on"));
            return;
        }

        String[] tokens = m.group(1).split("\\s+", -1);

        // If user runs any PL command, we turn PL mode on.
        plMode = true;

        if (tokens[0].equals("foreach")) {
            Matcher foreachM = foreachPattern.matcher(
                    dereference(token.val, false));
            if (!foreachM.matches()) {
                throw new BadSpecial(SqltoolRB.foreach_malformat.getString());
            }
            if (foreachM.groupCount() != 2) {
                throw new RuntimeException(
                        "Internal assertion failed.  "
                        + "foreach pattern matched, but captured "
                        + foreachM.groupCount() + " groups");
            }

            String varName   = foreachM.group(1);
            if (varName.indexOf(':') > -1) {
                throw new BadSpecial(SqltoolRB.plvar_nocolon.getString());
            }
            String[] values = foreachM.group(2).split("\\s+", -1);

            String origval = shared.userVars.get(varName);


            try {
                for (String val : values) {
                    try {
                        shared.userVars.put(varName, val);
                        updateUserSettings();

                        boolean origRecursed = recursed;
                        recursed = true;
                        try {
                            scanpass(token.nestedBlock.dup());
                        } finally {
                            recursed = origRecursed;
                        }
                    } catch (ContinueException ce) {
                        String ceMessage = ce.getMessage();

                        if (ceMessage != null
                                &&!ceMessage.equals("foreach")) {
                            throw ce;
                        }
                    }
                }
            } catch (BreakException be) {
                String beMessage = be.getMessage();

                // Handle "foreach" and plain breaks (by doing nothing)
                if (beMessage != null &&!beMessage.equals("foreach")) {
                    throw be;
                }
            } catch (QuitNow qn) {
                throw qn;
            } catch (RuntimeException re) {
                throw re;  // Unrecoverable
            } catch (Exception e) {
                throw new BadSpecial(SqltoolRB.pl_block_fail.getString(), e);
            }

            if (origval == null) {
                shared.userVars.remove(varName);
                updateUserSettings();
            } else {
                shared.userVars.put(varName, origval);
            }

            return;
        }

        if (tokens[0].equals("if") || tokens[0].equals("while")) {
            Matcher ifwhileM= ifwhilePattern.matcher(
                    dereference(token.val, false));
            if (!ifwhileM.matches()) {
                throw new BadSpecial(SqltoolRB.ifwhile_malformat.getString());
            }
            if (ifwhileM.groupCount() != 1) {
                throw new RuntimeException(
                        "Internal assertion failed.  "
                        + "if/while pattern matched, but captured "
                        + ifwhileM.groupCount() + " groups");
            }

            String[] values =
                    ifwhileM.group(1).replaceAll("!([a-zA-Z0-9*])", "! $1").
                        replaceAll("([a-zA-Z0-9*])!", "$1 !").split("\\s+", -1);

            if (tokens[0].equals("if")) {
                try {
                    if (eval(values)) {
                        boolean origRecursed = recursed;
                        recursed = true;
                        try {
                            scanpass(token.nestedBlock.dup());
                        } finally {
                            recursed = origRecursed;
                        }
                    }
                } catch (BreakException be) {
                    String beMessage = be.getMessage();

                    // Handle "if" and plain breaks (by doing nothing)
                    if (beMessage == null ||!beMessage.equals("if")) {
                        throw be;
                    }
                } catch (ContinueException ce) {
                    throw ce;
                } catch (QuitNow qn) {
                    throw qn;
                } catch (BadSpecial bs) {
                    bs.appendMessage(SqltoolRB.if_malformat.getString());
                    throw bs;
                } catch (RuntimeException re) {
                    throw re;  // Unrecoverable
                } catch (Exception e) {
                    throw new BadSpecial(
                        SqltoolRB.pl_block_fail.getString(), e);
                }
            } else if (tokens[0].equals("while")) {
                try {

                    while (eval(values)) {
                        try {
                            boolean origRecursed = recursed;
                            recursed = true;
                            try {
                                scanpass(token.nestedBlock.dup());
                            } finally {
                                recursed = origRecursed;
                            }
                        } catch (ContinueException ce) {
                            String ceMessage = ce.getMessage();

                            if (ceMessage != null &&!ceMessage.equals("while")) {
                                throw ce;
                            }
                        }
                    }
                } catch (BreakException be) {
                    String beMessage = be.getMessage();

                    // Handle "while" and plain breaks (by doing nothing)
                    if (beMessage != null &&!beMessage.equals("while")) {
                        throw be;
                    }
                } catch (QuitNow qn) {
                    throw qn;
                } catch (BadSpecial bs) {
                    bs.appendMessage(SqltoolRB.while_malformat.getString());
                    throw bs;
                } catch (RuntimeException re) {
                    throw re;  // Unrecoverable
                } catch (Exception e) {
                    throw new BadSpecial(
                            SqltoolRB.pl_block_fail.getString(), e);
                }
            } else {
                // Assertion
                throw new RuntimeException(
                        SqltoolRB.pl_unknown.getString(tokens[0]));
            }

            return;
        }

        throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0]));
    }

    /**
     * Process a Non-Block Process Language Command.
     * Nesting not supported yet.
     *
     * @param inString  Trimmed non-null command without leading *
     *                  (may be empty string "").
     * @throws BadSpecial special-command-specific errors.
     * @throws SqlToolError all other errors, plus BreakException and
     *                      ContinueException.
     */
    private void processPL(String inString) throws BadSpecial, SqlToolError {
        String string = (inString == null) ? buffer.val : inString;
        Matcher m = plPattern.matcher(dereference(string, false));
        if (!m.matches()) {
            throw new BadSpecial(SqltoolRB.pl_malformat.getString());
            // I think it's impossible to get here, since the pattern is
            // so liberal.
        }
        if (m.groupCount() < 1 || m.group(1) == null) {
            plMode = true;
            stdprintln(SqltoolRB.pl_expansionmode.getString("on"));
            return;
        }

        String[] tokens = m.group(1).split("\\s+", -1);

        if (tokens[0].charAt(0) == '?') {
            stdprintln(SqltoolRB.pl_help.getString());

            return;
        }

        // If user runs any PL command, we turn PL mode on.
        plMode = true;

        if (tokens[0].equals("end")) {
            throw new BadSpecial(SqltoolRB.end_noblock.getString());
        }

        if (tokens[0].equals("continue")) {
            if (tokens.length > 1) {
                if (tokens.length == 2 &&
                        (tokens[1].equals("foreach") ||
                         tokens[1].equals("while"))) {
                    throw new ContinueException(tokens[1]);
                }
                throw new BadSpecial(SqltoolRB.continue_syntax.getString());
            }

            throw new ContinueException();
        }

        if (tokens[0].equals("break")) {
            if (tokens.length > 1) {
                if (tokens.length == 2 &&
                        (tokens[1].equals("foreach") ||
                         tokens[1].equals("if") ||
                         tokens[1].equals("while") ||
                         tokens[1].equals("file"))) {
                    throw new BreakException(tokens[1]);
                }
                throw new BadSpecial(SqltoolRB.break_syntax.getString());
            }

            throw new BreakException();
        }

        if (tokens[0].equals("list") || tokens[0].equals("listvalues")
                || tokens[0].equals("listsysprops")) {
            boolean sysProps =tokens[0].equals("listsysprops");
            String  s;
            boolean doValues = (tokens[0].equals("listvalues") || sysProps);
            // Always list System Property values.
            // They are unlikely to be very long, like PL variables may be.

            if (tokens.length == 1) {
                stdprint(formatNicely(
                        (sysProps ? System.getProperties() : shared.userVars),
                        doValues));
            } else {
                if (doValues) {
                    stdprintln(SqltoolRB.pl_list_parens.getString());
                } else {
                    stdprintln(SqltoolRB.pl_list_lengths.getString());
                }

                for (String token : tokens) {
                    s = (String) (sysProps ? System.getProperties()
                                           : shared.userVars).get(token);
                    if (s == null) continue;
                    stdprintln("    " + token + ": "
                               + (doValues ? ("(" + s + ')')
                                           : Integer.toString(s.length())));
                }
            }

            return;
        }

        if (tokens[0].equals("dump") || tokens[0].equals("load")) {
            if (tokens.length != 3) {
                throw new BadSpecial(SqltoolRB.dumpload_malformat.getString());
            }

            String varName = tokens[1];

            if (varName.indexOf(':') > -1) {
                throw new BadSpecial(SqltoolRB.plvar_nocolon.getString());
            }
            File   dlFile    = new File(tokens[2]);

            try {
                if (tokens[0].equals("dump")) {
                    dump(varName, dlFile);
                } else {
                    load(varName, dlFile, shared.encoding);
                }
            } catch (IOException ioe) {
                throw new BadSpecial(SqltoolRB.dumpload_fail.getString(
                        varName, dlFile.toString()), ioe);
            }

            return;
        }

        if (tokens[0].equals("prepare")) {
            if (tokens.length != 2) {
                throw new BadSpecial(SqltoolRB.prepare_malformat.getString());
            }

            if (shared.userVars.get(tokens[1]) == null) {
                throw new BadSpecial(
                        SqltoolRB.plvar_undefined.getString(tokens[1]));
            }

            prepareVar = tokens[1];
            doPrepare  = true;

            return;
        }

        m = varsetPattern.matcher(dereference(string, false));
        if (!m.matches()) {
            throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0]));
        }
        if (m.groupCount() < 2 || m.groupCount() > 3) {
            // Assertion
            throw new RuntimeException("varset pattern matched but captured "
                    + m.groupCount() + " groups");
        }

        String varName  = m.group(1);

        if (varName.indexOf(':') > -1) {
            throw new BadSpecial(SqltoolRB.plvar_nocolon.getString());
        }

        switch (m.group(2).charAt(0)) {
            case '_' :
                silentFetch = true;
            case '~' :
                if (m.groupCount() > 2 && m.group(3) != null) {
                    throw new BadSpecial(SqltoolRB.plvar_tildedash_nomoreargs.getString(
                            m.group(3)));
                }

                shared.userVars.remove(varName);
                updateUserSettings();

                fetchingVar = varName;

                return;

            case '=' :
                if (fetchingVar != null && fetchingVar.equals(varName)) {
                    fetchingVar = null;
                }

                if (varName.equals("*ENCODING")) try {
                    // Special case so we can proactively prohibit encodings
                    // which will not work, so we'll always be confident
                    // that 'encoding' value is always good.
                    setEncoding(m.group(3));
                    return;
                } catch (UnsupportedEncodingException use) {
                    throw new BadSpecial(
                            SqltoolRB.encode_fail.getString(m.group(3)));
                }
                if (m.groupCount() > 2 && m.group(3) != null) {
                    shared.userVars.put(varName, m.group(3));
                } else {
                    shared.userVars.remove(varName);
                }
                updateUserSettings();

                return;
        }

        throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0]));
        // I think this would already be caught in the setvar block above.
    }

    /**
     * Wrapper methods so don't need to call x(..., false) in most cases.
     */
    /* Unused.  Enable when/if need.
    private void stdprintln() {
        stdprintln(false);
    }
    */

    private void stdprint(String s) {
        stdprint(s, false);
    }

    private void stdprintln(String s) {
        stdprintln(s, false);
    }

    /**
     * Encapsulates normal output.
     *
     * Conditionally HTML-ifies output.
     */
    private void stdprintln(boolean queryOutput) {
        if (shared.psStd != null) if (htmlMode) {
            shared.psStd.println("<BR>");
        } else {
            shared.psStd.println();
        }

        if (queryOutput && pwQuery != null) {
            if (htmlMode) {
                pwQuery.println("<BR>");
            } else {
                pwQuery.println();
            }

            pwQuery.flush();
        }
    }

    /**
     * Encapsulates error output.
     *
     * Conditionally HTML-ifies error output.
     */
    private void errprintln(String s) {
        if (shared.psStd != null && htmlMode) {
            shared.psStd.println("<DIV style='color:white; background: red; "
                       + "font-weight: bold'>" + s + "</DIV>");
        } else {
            logger.privlog(Level.SEVERE, s, null, 5, SqlFile.class);
            /* Only consistent way we can log source location is to log
             * the caller of SqlFile.
             * This seems acceptable, since the location being reported
             * here is not the source of the problem anyways.  */
        }
    }

    /**
     * Encapsulates normal output.
     *
     * Conditionally HTML-ifies output.
     */
    private void stdprint(String s, boolean queryOutput) {
        if (shared.psStd != null)
            shared.psStd.print(htmlMode ? ("<P>" + s + "</P>") : s);

        if (queryOutput && pwQuery != null) {
            pwQuery.print(htmlMode ? ("<P>" + s + "</P>") : s);
            pwQuery.flush();
        }
    }

    /**
     * Encapsulates normal output.
     *
     * Conditionally HTML-ifies output.
     */
    private void stdprintln(String s, boolean queryOutput) {
        shared.psStd.println(htmlMode ? ("<P>" + s + "</P>")
                               : s);

        if (queryOutput && pwQuery != null) {
            pwQuery.println(htmlMode ? ("<P>" + s + "</P>")
                                     : s);
            pwQuery.flush();
        }
    }

    // Just because users may be used to seeing "[null]" in normal
    // SqlFile output, we use the same default value for null in DSV
    // files, but this DSV null representation can be changed to anything.
    private static final String DEFAULT_NULL_REP = "[null]";
    private static final String DEFAULT_ROW_DELIM = LS;
    private static final String DEFAULT_ROW_SPLITTER = "\\r\\n|\\r|\\n";
    private static final String DEFAULT_COL_DELIM = "|";
    private static final String DEFAULT_COL_SPLITTER = "\\|";
    private static final String DEFAULT_SKIP_PREFIX = "#";
    private static final int    DEFAULT_ELEMENT   = 0,
                                HSQLDB_ELEMENT    = 1,
                                ORACLE_ELEMENT    = 2
    ;

    // These do not specify order listed, just inclusion.
    private static final int[] listMDSchemaCols = { 1 };
    private static final int[] listMDIndexCols  = {
        2, 6, 3, 9, 4, 10, 11
    };

    /** Column numbering starting at 1. */
    private static final int[][] listMDTableCols = {
        {
            2, 3
        },    // Default
        {
            2, 3
        },    // HSQLDB
        {
            2, 3
        },    // Oracle
    };

    /**
     * SYS and SYSTEM are the only base system accounts in Oracle, however,
     * from an empirical perspective, all of these other accounts are
     * system accounts because <UL>
     * <LI> they are hidden from the casual user
     * <LI> they are created by the installer at installation-time
     * <LI> they are used automatically by the Oracle engine when the
     *      specific Oracle sub-product is used
     * <LI> the accounts should not be <I>messed with</I> by database users
     * <LI> the accounts should certainly not be used if the specific
     *      Oracle sub-product is going to be used.
     * </UL>
     *
     * General advice:  If you aren't going to use an Oracle sub-product,
     * then <B>don't install it!</B>
     * Don't blindly accept default when running OUI.
     *
     * If users also see accounts that they didn't create with names like
     * SCOTT, ADAMS, JONES, CLARK, BLAKE, OE, PM, SH, QS, QS_*, these
     * contain sample data and the schemas can safely be removed.
     */
    private static final String[] oracleSysSchemas = {
        "SYS", "SYSTEM", "OUTLN", "DBSNMP", "OUTLN", "MDSYS", "ORDSYS",
        "ORDPLUGINS", "CTXSYS", "DSSYS", "PERFSTAT", "WKPROXY", "WKSYS",
        "WMSYS", "XDB", "ANONYMOUS", "ODM", "ODM_MTR", "OLAPSYS", "TRACESVR",
        "REPADMIN"
    };

    public String getCurrentSchema() throws BadSpecial, SqlToolError {
        requireConnection();
        Statement st = null;
        ResultSet rs = null;
        try {
            st = shared.jdbcConn.createStatement();
            rs = st.executeQuery("VALUES CURRENT_SCHEMA");
            if (!rs.next())
                throw new BadSpecial(SqltoolRB.no_vendor_schemaspt.getString());
            String currentSchema = rs.getString(1);
            if (currentSchema == null)
                throw new BadSpecial(
                        SqltoolRB.schemaname_retrieval_fail.getString());
            return currentSchema;
        } catch (SQLException se) {
            throw new BadSpecial(SqltoolRB.no_vendor_schemaspt.getString());
        } finally {
            if (rs != null) try {
                rs.close();
            } catch (SQLException se) {
                // Purposefully doing nothing
            } finally {
                rs = null;
            }
            if (st != null) try {
                st.close();
            } catch (SQLException se) {
                // Purposefully doing nothing
            } finally {
                st = null;
            }
        }
    }

    /**
     * Lists available database tables.
     *
     * Filter handling is admittedly inconsistent, both wrt pattern
     * matching (java.util.regex vs. DB-implemented matching) and
     * which columns the filter is matched against.
     * The former is because, for performance and because the DB should
     * know best how to supply the desired results, we need to let the
     * database do filtering if at all possible.
     * In many cases, the DB does not have a filter option, so we have
     * to filter ourselves.
     * For the latter, we have no control over which columsn the DB
     * matches agains, plus the displayResultSet() method in this class
     * can only match against all columns (only reason not to add
     * column-specific filtering is to keep the complexity manageable).
     *
     * @throws BadSpecial usually wrap a cause (which cause is a
     *                    SQLException in some cases).
     * @throws SqlToolError passed through from other methods in this class.
     */
    private void listTables(char c, String inFilter) throws BadSpecial,
            SqlToolError {
        requireConnection();
        String   schema  = null;
        int[]    listSet = null;
        String[] types   = null;

        /** For workaround for \T for Oracle */
        String[] additionalSchemas = null;

        /** This is for specific non-getTable() queries */
        Statement statement = null;
        ResultSet rs        = null;
        String    narrower  = "";
        /*
         * Doing case-sensitive filters now, for greater portability.
        String                    filter = ((inFilter == null)
                                          ? null : inFilter.toUpperCase());
         */
        String filter = inFilter;

        try {
            DatabaseMetaData md            = shared.jdbcConn.getMetaData();
            String           dbProductName = md.getDatabaseProductName();
            int              majorVersion  = 0;
            int              minorVersion  = 0;

            // We only use majorVersion and minorVersion for HyperSQL so far
            // The calls avoided here avoid problems with non-confirmant drivers
            if (dbProductName.indexOf("HSQL") > -1) try {
                majorVersion  = md.getDatabaseMajorVersion();
                minorVersion  = md.getDatabaseMinorVersion();
            } catch (UnsupportedOperationException uoe) {
                // It seems that Sun's JDBC/ODBC bridge throws here
                majorVersion = 2;
                minorVersion = 0;
            }

            //System.err.println("DB NAME = (" + dbProductName + ')');
            // Database-specific table filtering.

            /* 3 Types of actions:
             *    1) Special handling.  Return from the "case" block directly.
             *    2) Execute a specific query.  Set statement in the "case".
             *    3) Otherwise, set filter info for dbmd.getTable() in the
             *       "case".
             */
            types = new String[1];

            switch (c) {
                case '*' :
                    types = null;
                    break;

                case 'S' :
                    if (dbProductName.indexOf("Oracle") > -1) {
                        errprintln(SqltoolRB.vendor_oracle_dS.getString());

                        types[0]          = "TABLE";
                        schema            = "SYS";
                        additionalSchemas = oracleSysSchemas;
                    } else {
                        types[0] = "SYSTEM TABLE";
                    }
                    break;

                case 's' :
                    if (dbProductName.indexOf("HSQL") > -1) {
                        //  HSQLDB does not consider Sequences as "tables",
                        //  hence we do not list them in
                        //  DatabaseMetaData.getTables().
                        if (filter != null) {
                            Matcher matcher = dotPattern.matcher(filter);
                            if (matcher.matches()) {
                                filter = (matcher.group(2).length() > 0)
                                        ? matcher.group(2) : null;
                                narrower = "\nWHERE sequence_schema = '"
                                        + ((matcher.group(1).length() > 0)
                                                ? matcher.group(1)
                                                : getCurrentSchema()) + "'";
                            }
                        }

                        statement = shared.jdbcConn.createStatement();

                        statement.execute(
                            "SELECT sequence_schema, sequence_name FROM "
                            + "information_schema."
                            + ((minorVersion> 8 || majorVersion > 1)
                            ? "sequences" : "system_sequences") + narrower);
                    } else {
                        types[0] = "SEQUENCE";
                    }
                    break;

                case 'r' :
                    if (dbProductName.indexOf("HSQL") > -1) {
                        statement = shared.jdbcConn.createStatement();

                        statement.execute(
                            "SELECT authorization_name FROM information_schema."
                            + ((minorVersion> 8 || majorVersion > 1)
                            ? "authorizations" : "system_authorizations")
                            + "\nWHERE authorization_type = 'ROLE'\n"
                            + "ORDER BY authorization_name");
                    } else if (dbProductName.indexOf(
                            "Adaptive Server Enterprise") > -1) {
                        // This is the basic Sybase server.  Sybase also has
                        // their "Anywhere", ASA (for embedded), and replication
                        // databases, but I don't know the Metadata strings for
                        // those.
                        statement = shared.jdbcConn.createStatement();

                        statement.execute(
                            "SELECT name FROM syssrvroles ORDER BY name");
                    } else if (dbProductName.indexOf(
                            "Apache Derby") > -1) {
                        throw new BadSpecial(
                            SqltoolRB.vendor_derby_dr.getString());
                    } else {
                        throw new BadSpecial(
                            SqltoolRB.vendor_nosup_d.getString("r"));
                    }
                    break;

                case 'u' :
                    if (dbProductName.indexOf("HSQL") > -1) {
                        statement = shared.jdbcConn.createStatement();

                        statement.execute("SELECT "
                            + ((minorVersion> 8 || majorVersion > 1)
                            ? "user_name" : "user") + ", admin FROM "
                            + "information_schema.system_users\n"
                            + "ORDER BY user_name");
                    } else if (dbProductName.indexOf("Oracle") > -1) {
                        statement = shared.jdbcConn.createStatement();

                        statement.execute(
                            "SELECT username, created FROM all_users "
                            + "ORDER BY username");
                    } else if (dbProductName.indexOf("PostgreSQL") > -1) {
                        statement = shared.jdbcConn.createStatement();

                        statement.execute(
                            "SELECT usename, usesuper FROM pg_catalog.pg_user "
                            + "ORDER BY usename");
                    } else if (dbProductName.indexOf(
                            "Adaptive Server Enterprise") > -1) {
                        // This is the basic Sybase server.  Sybase also has
                        // their "Anywhere", ASA (for embedded), and replication
                        // databases, but I don't know the Metadata strings for
                        // those.
                        statement = shared.jdbcConn.createStatement();

                        statement.execute(
                            "SELECT name, accdate, fullname FROM syslogins "
                            + "ORDER BY name");
                    } else if (dbProductName.indexOf(
                            "Apache Derby") > -1) {
                        throw new BadSpecial(
                            SqltoolRB.vendor_derby_du.getString());
                    } else {
                        throw new BadSpecial(
                            SqltoolRB.vendor_nosup_d.getString("u"));
                    }
                    break;

                case 'a' :
                    if (dbProductName.indexOf("HSQL") > -1
                        && (minorVersion < 9 && majorVersion < 2)) {
                        // HSQLDB after 1.8 doesn't support any type of aliases
                        //  Earlier HSQLDB Aliases are not the same things as
                        //  the aliases listed in DatabaseMetaData.getTables().
                        if (filter != null) {
                            Matcher matcher = dotPattern.matcher(filter);
                            if (matcher.matches()) {
                                filter = (matcher.group(2).length() > 0)
                                        ? matcher.group(2) : null;
                                narrower = "\nWHERE alias_schema = '"
                                        + ((matcher.group(1).length() > 0)
                                                ? matcher.group(1)
                                                : getCurrentSchema()) + "'";
                            }
                        }

                        statement = shared.jdbcConn.createStatement();

                        statement.execute(
                            "SELECT alias_schem, alias FROM "
                            + "information_schema.system_aliases" + narrower);
                    } else {
                        types[0] = "ALIAS";
                    }
                    break;

                case 't' :
                    excludeSysSchemas = (dbProductName.indexOf("Oracle")
                                         > -1);
                    types[0] = "TABLE";
                    break;

                case 'v' :
                    types[0] = "VIEW";
                    break;

                case 'n' :
                    rs = md.getSchemas();

                    if (rs == null) {
                        throw new BadSpecial(
                            "Failed to get metadata from database");
                    }

                    displayResultSet(null, rs, listMDSchemaCols, filter);

                    return;

                case 'i' :

                    // Some databases require to specify table, some don't.
                    /*
                    if (filter == null) {
                        throw new BadSpecial("You must specify the index's "
                                + "table as argument to \\di");
                    }
                     */
                    String table = null;

                    if (filter != null) {
                        Matcher matcher = dotPattern.matcher(filter);
                        if (matcher.matches()) {
                            table = (matcher.group(2).length() > 0)
                                    ? matcher.group(2) : null;
                            schema = (matcher.group(1).length() > 0)
                                    ? matcher.group(1) : getCurrentSchema();
                        } else {
                            table = filter;
                        }
                        filter = null;
                    }

                    // N.b. Oracle incorrectly reports the INDEX SCHEMA as
                    // the TABLE SCHEMA.  The Metadata structure seems to
                    // be designed with the assumption that the INDEX schema
                    // will be the same as the TABLE schema.
                    rs = md.getIndexInfo(null, schema, table, false, true);

                    if (rs == null) {
                        throw new BadSpecial(
                            "Failed to get metadata from database");
                    }

                    displayResultSet(null, rs, listMDIndexCols, null);

                    return;

                default :
                    throw new BadSpecial(SqltoolRB.special_d_unknown.getString(
                            Character.toString(c)) + LS + D_OPTIONS_TEXT);
            }

            if (statement == null) {
                if (dbProductName.indexOf("HSQL") > -1) {
                    listSet = listMDTableCols[HSQLDB_ELEMENT];
                } else if (dbProductName.indexOf("Oracle") > -1) {
                    listSet = listMDTableCols[ORACLE_ELEMENT];
                } else {
                    listSet = listMDTableCols[DEFAULT_ELEMENT];
                }


                if (schema == null && filter != null) {
                    Matcher matcher = dotPattern.matcher(filter);
                    if (matcher.matches()) {
                        filter = (matcher.group(2).length() > 0)
                                ? matcher.group(2) : null;
                        schema = (matcher.group(1).length() > 0)
                                ? matcher.group(1)
                                : getCurrentSchema();
                    }
                }
            }

            rs = ((statement == null)
                  ? md.getTables(null, schema, null, types)
                  : statement.getResultSet());

            if (rs == null) {
                throw new BadSpecial(SqltoolRB.metadata_fetch_fail.getString());
            }

            displayResultSet(null, rs, listSet, filter);

            if (additionalSchemas != null) {
                for (String additionalSchema : additionalSchemas) {
                    /*
                     * Inefficient, but we have to do each successful query
                     * twice in order to prevent calling displayResultSet
                     * for empty/non-existent schemas
                     */
                    rs = md.getTables(null, additionalSchema, null,
                                      types);

                    if (rs == null) {
                        throw new BadSpecial(
                                SqltoolRB.metadata_fetch_failfor.getString(
                                additionalSchema));
                    }

                    if (!rs.next()) {
                        continue;
                    }

                    displayResultSet(
                        null,
                        md.getTables(
                            null, additionalSchema, null, types), listSet, filter);
                }
            }
        } catch (SQLException se) {
            throw new BadSpecial(SqltoolRB.metadata_fetch_fail.getString(), se);
        } catch (NullPointerException npe) {
            throw new BadSpecial(SqltoolRB.metadata_fetch_fail.getString(),
                    npe);
        } finally {
            excludeSysSchemas = false;

            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException se) {
                    // We already got what we want from it, or have/are
                    // processing a more specific error.
                }
            }

            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException se) {
                    // Purposefully doing nothing
                }
            }
        }
    }

    private boolean excludeSysSchemas;

    /**
     * Process the contents of Edit Buffer as an SQL Statement
     *
     * @throws SQLException thrown by JDBC driver.
     * @throws SqlToolError all other errors.
     */
    private void processSQL() throws SQLException, SqlToolError {
        requireConnection();
        if (buffer == null)
            throw new RuntimeException(
                    "Internal assertion failed.  No buffer in processSQL().");
        if (buffer.type != Token.SQL_TYPE)
            throw new RuntimeException(
                    "Internal assertion failed.  "
                    + "Token type " + buffer.getTypeString()
                    + " in processSQL().");
        // No reason to check autoCommit constantly.  If we need to roll
        // back, we will check the autocommit state at that time.
        lastSqlStatement    = (plMode ? dereference(buffer.val, true)
                                      : buffer.val);
        // N.b. "lastSqlStatement" is a misnomer only inside this method.
        // Outside of this method, this var references the "last" SQL
        // statement which we attempted to execute.
        if ((!permitEmptySqlStatements) && buffer.val == null
                || buffer.val.trim().length() < 1) {
            throw new SqlToolError(SqltoolRB.sqlstatement_empty.getString());
            // There is nothing inherently wrong with issuing
            // an empty command, like to test DB server health.
            // But, this check effectively catches many syntax
            // errors early.
        }
        Statement statement = null;

        long startTime = 0;
        if (reportTimes) startTime = (new java.util.Date()).getTime();
        try { // VERY outer block just to ensure we close "statement"
        try { if (doPrepare) {
            if (lastSqlStatement.indexOf('?') < 1) {
                lastSqlStatement = null;
                throw new SqlToolError(SqltoolRB.prepare_demandqm.getString());
            }

            doPrepare = false;

            PreparedStatement ps =
                    shared.jdbcConn.prepareStatement(lastSqlStatement);
            statement = ps;

            if (prepareVar == null) {
                if (binBuffer == null) {
                    lastSqlStatement = null;
                    throw new SqlToolError(
                            SqltoolRB.binbuffer_empty.getString());
                }

                ps.setBytes(1, binBuffer);
            } else {
                String val = shared.userVars.get(prepareVar);

                if (val == null) {
                    lastSqlStatement = null;
                    throw new SqlToolError(
                            SqltoolRB.plvar_undefined.getString(prepareVar));
                }

                prepareVar = null;

                ps.setString(1, val);
            }

            ps.executeUpdate();
        } else {
            statement = shared.jdbcConn.createStatement();

            statement.execute(lastSqlStatement);
        } } finally {
            if (reportTimes) {
                long elapsed = (new java.util.Date().getTime()) - startTime;
                //condlPrintln("</TABLE>", true);
                condlPrintln(SqltoolRB.exectime_report.getString(
                        (int) elapsed), false);
            }
        }

        /* This catches about the only very safe way to know a COMMIT
         * is not needed. */
        try {
            shared.possiblyUncommitteds = !shared.jdbcConn.getAutoCommit()
                    && !commitOccursPattern.matcher(lastSqlStatement).matches();
        } catch (java.sql.SQLException se) {
            // If connection is closed by instance shutdown or whatever, we'll
            // get here.
            lastSqlStatement = null; // I forget what this is for
            try {
                shared.jdbcConn.close();
            } catch (Exception anye) {
                // Intentionally empty
            }
            shared.jdbcConn = null;
            shared.possiblyUncommitteds = false;
            stdprintln(SqltoolRB.disconnect_success.getString());
            return;
        }
        ResultSet rs = null;
        try {
            rs = statement.getResultSet();
            displayResultSet(statement, rs, null, null);
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException se) {
                    // We already got what we want from it, or have/are
                    // processing a more specific error.
                }
            }
        }
        } finally {
            try {
                if (statement != null) statement.close();
            } catch (SQLException se) {
                // Purposefully doing nothing
            }
        }
        lastSqlStatement = null;
    }

    /**
     * Display the given result set for user.
     * The last 3 params are to narrow down records and columns where
     * that can not be done with a where clause (like in metadata queries).
     * <P/>
     * Caller is responsible for closing any passed Statement or ResultSet.
     *
     * @param statement The SQL Statement that the result set is for.
     *                  (This is so we can get the statement's update count.
     *                  Can be null for non-update queries.)
     * @param r         The ResultSet to display.
     * @param incCols   Optional list of which columns to include (i.e., if
     *                  given, then other columns will be skipped).
     * @param filterRegex Optional filter.  Rows are skipped which to not
     *                  contain this substring in ANY COLUMN.
     *                  (Should add another param to specify targeted columns).
     * @throws SQLException thrown by JDBC driver.
     * @throws SqlToolError all other errors.
     */
    private void displayResultSet(Statement statement, ResultSet r,
                                  int[] incCols,
                                  String filterString) throws SQLException,
                                  SqlToolError {
        java.sql.Timestamp ts;
        int dotAt;
        int                updateCount = (statement == null) ? -1
                                                             : statement
                                                                 .getUpdateCount();
        boolean            silent      = silentFetch;
        boolean            binary      = fetchBinary;
        Pattern            filter = null;

        silentFetch = false;
        fetchBinary = false;

        if (filterString != null) try {
            filter = Pattern.compile(filterString);
        } catch (PatternSyntaxException pse) {
            throw new SqlToolError(
                    SqltoolRB.regex_malformat.getString(pse.getMessage()));
        }

        if (excludeSysSchemas) {
            stdprintln(SqltoolRB.vendor_nosup_sysschemas.getString());
        }

        switch (updateCount) {
            case -1 :
                if (r == null) {
                    stdprintln(SqltoolRB.noresult.getString(), true);

                    break;
                }

                ResultSetMetaData m        = r.getMetaData();
                int               cols     = m.getColumnCount();
                int               incCount = (incCols == null) ? cols
                                                               : incCols
                                                                   .length;
                String            val;
                List<String[]>    rows        = new ArrayList<String[]>();
                String[]          headerArray = null;
                String[]          fieldArray;
                int[]             maxWidth = new int[incCount];
                int               insi;
                boolean           skip;
                boolean           isValNull;

                // STEP 1: GATHER DATA
                if (!htmlMode) {
                    for (int i = 0; i < maxWidth.length; i++) {
                        maxWidth[i] = 0;
                    }
                }

                boolean[] rightJust = new boolean[incCount];
                int[]     dataType  = new int[incCount];
                boolean[] autonulls = new boolean[incCount];

                insi        = -1;
                headerArray = new String[incCount];

                for (int i = 1; i <= cols; i++) {
                    if (incCols != null) {
                        skip = true;

                        for (int j = 0; j < incCols.length; j++) {
                            if (i == incCols[j]) {
                                skip = false;
                            }
                        }

                        if (skip) {
                            continue;
                        }
                    }

                    headerArray[++insi] = m.getColumnLabel(i);
                    dataType[insi]      = m.getColumnType(i);
                    rightJust[insi]     = false;
                    autonulls[insi]     = true;
                    // This is what we want for java.sql.Types.ARRAY :

                    switch (dataType[insi]) {
                        case java.sql.Types.BIGINT :
                        case java.sql.Types.BIT :
                        case java.sql.Types.DECIMAL :
                        case java.sql.Types.DOUBLE :
                        case java.sql.Types.FLOAT :
                        case java.sql.Types.INTEGER :
                        case java.sql.Types.NUMERIC :
                        case java.sql.Types.REAL :
                        case java.sql.Types.SMALLINT :
                        case java.sql.Types.TINYINT :
                            rightJust[insi] = true;
                            break;

                        case java.sql.Types.VARBINARY :
                        case java.sql.Types.VARCHAR :
                        case java.sql.Types.BLOB :
                        case java.sql.Types.CLOB :
                        case java.sql.Types.LONGVARBINARY :
                        case java.sql.Types.LONGVARCHAR :
                            autonulls[insi] = false;
                            break;
                    }

                    if (htmlMode) {
                        continue;
                    }

                    if (headerArray[insi] != null
                            && headerArray[insi].length() > maxWidth[insi]) {
                        maxWidth[insi] = headerArray[insi].length();
                    }
                }

                boolean filteredOut;

                while (r.next()) {
                    fieldArray  = new String[incCount];
                    insi        = -1;
                    filteredOut = filter != null;

                    for (int i = 1; i <= cols; i++) {
                        // This is the only case where we can save a data
                        // read by recognizing we don't need this datum early.
                        if (incCols != null) {
                            skip = true;

                            for (int incCol : incCols) {
                                if (i == incCol) {
                                    skip = false;
                                }
                            }

                            if (skip) {
                                continue;
                            }
                        }

                        // This row may still be ditched, but it is now
                        // certain that we need to increment the fieldArray
                        // index.
                        ++insi;

                        if (!SqlFile.canDisplayType(dataType[insi])) {
                            binary = true;
                        }

                        val = null;
                        isValNull = true;

                        if (!binary) {
                            /*
                             * The special formatting for all time-related
                             * fields is because the most popular current
                             * databases are extremely inconsistent about
                             * what resolution is returned for the same types.
                             * In my experience so far, Dates MAY have
                             * resolution down to second, but only TIMESTAMPs
                             * support sub-second res. (and always can).
                             * On top of that there is no consistency across
                             * getObject().toString().  Oracle doesn't even
                             * implement it for their custom TIMESTAMP type.
                             */
                            switch (dataType[insi]) {
                                case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE:
                                case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE:
                                case java.sql.Types.TIMESTAMP:
                                case java.sql.Types.DATE:
                                case java.sql.Types.TIME:
                                    ts  = r.getTimestamp(i);
                                    isValNull = r.wasNull();
                                    val = ((ts == null) ? null : ts.toString());
                                    // Following block truncates non-zero
                                    // sub-seconds from time types OTHER than
                                    // TIMESTAMP.
                                    if (dataType[insi]
                                            != java.sql.Types.TIMESTAMP
                                            && dataType[insi]
                                            != org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE
                                            && val != null) {
                                        dotAt = val.lastIndexOf('.');
                                        for (int z = dotAt + 1;
                                                z < val.length(); z++) {
                                            if (val.charAt(z) != '0') {
                                                dotAt = 0;
                                                break;
                                            }
                                        }
                                        if (dotAt > 1) {
                                            val = val.substring(0, dotAt);
                                        }
                                    }
                                    break;
                                default:
                                    val = r.getString(i);
                                    isValNull = r.wasNull();

                                    // If we tried to get a String but it
                                    // failed, try getting it with a String
                                    // Stream
                                    if (val == null) {
                                        try {
                                            val = streamToString(
                                                r.getAsciiStream(i),
                                                shared.encoding);
                                            isValNull = r.wasNull();
                                        } catch (Exception e) {
                                            // This isn't an error.
                                            // We are attempting to do a stream
                                            // fetch if-and-only-if the column
                                            // supports it.
                                        }
                                    }
                            }
                        }

                        if (binary || (val == null &&!isValNull)) {
                            if (pwDsv != null) {
                                throw new SqlToolError(
                                        SqltoolRB.dsv_bincol.getString());
                            }

                            // DB has a value but we either explicitly want
                            // it as binary, or we failed to get it as String.
                            try {
                                binBuffer =
                                    SqlFile.streamToBytes(r.getBinaryStream(i));
                                isValNull = r.wasNull();
                            } catch (IOException ioe) {
                                throw new SqlToolError(
                                    "Failed to read value using stream",
                                    ioe);
                            }

                            stdprintln(SqltoolRB.binbuf_write.getString(
                                       Integer.toString(binBuffer.length),
                                       headerArray[insi],
                                       SqlFile.sqlTypeToString(dataType[insi])
                                    ));

                            return;
                        }

                        if (excludeSysSchemas && val != null && i == 2) {
                            for (String oracleSysSchema : oracleSysSchemas) {
                                if (val.equals(oracleSysSchema)) {
                                    filteredOut = true;

                                    break;
                                }
                            }
                        }

                        shared.userVars.put("?",
                                ((val == null) ? nullRepToken : val));
                        if (fetchingVar != null) {
                            shared.userVars.put(
                                    fetchingVar, shared.userVars.get("?"));
                            updateUserSettings();

                            fetchingVar = null;
                        }

                        if (silent) {
                            return;
                        }

                        // We do not omit rows here.  We collect information
                        // so we can make the decision after all rows are
                        // read in.
                        if (filter != null
                            && (val == null || filter.matcher(val).find())) {
                            filteredOut = false;
                        }

                        ///////////////////////////////
                        // A little tricky here.  fieldArray[] MUST get set.
                        if (val == null && pwDsv == null) {
                            if (dataType[insi] == java.sql.Types.VARCHAR) {
                                fieldArray[insi] = (htmlMode ? "<I>null</I>"
                                                             : nullRepToken);
                            } else {
                                fieldArray[insi] = "";
                            }
                        } else {
                            fieldArray[insi] = val;
                        }

                        ///////////////////////////////
                        if (htmlMode || pwDsv != null) {
                            continue;
                        }

                        if (fieldArray[insi].length() > maxWidth[insi]) {
                            maxWidth[insi] = fieldArray[insi].length();
                        }
                    }

                    if (!filteredOut) {
                        rows.add(fieldArray);
                    }
                }

                // STEP 2: DISPLAY DATA  (= 2a OR 2b)
                // STEP 2a (Non-DSV)
                if (pwDsv == null) {
                    condlPrintln("<TABLE border='1'>", true);

                    if (incCount > 1) {
                        condlPrint(SqlFile.htmlRow(COL_HEAD) + LS + PRE_TD, true);

                        for (int i = 0; i < headerArray.length; i++) {
                            condlPrint("<TD>" + headerArray[i] + "</TD>",
                                       true);
                            condlPrint(((i > 0) ? "  " : "")
                                    + ((i < headerArray.length - 1
                                        || rightJust[i])
                                       ? StringUtil.toPaddedString(
                                         headerArray[i], maxWidth[i],
                                         ' ', !rightJust[i])
                                       : headerArray[i])
                                    , false);
                        }

                        condlPrintln(LS + PRE_TR + "</TR>", true);
                        condlPrintln("", false);

                        if (!htmlMode) {
                            for (int i = 0; i < headerArray.length; i++) {
                                condlPrint(((i > 0) ? "  "
                                                    : "") + SqlFile.divider(
                                                        maxWidth[i]), false);
                            }

                            condlPrintln("", false);
                        }
                    }

                    for (int i = 0; i < rows.size(); i++) {
                        condlPrint(SqlFile.htmlRow(((i % 2) == 0) ? COL_EVEN
                                                          : COL_ODD) + LS
                                                          + PRE_TD, true);

                        fieldArray = rows.get(i);

                        for (int j = 0; j < fieldArray.length; j++) {
                            condlPrint("<TD>" + fieldArray[j] + "</TD>",
                                       true);
                            condlPrint(((j > 0) ? "  " : "")
                                    + ((j < fieldArray.length - 1
                                        || rightJust[j])
                                       ? StringUtil.toPaddedString(
                                         fieldArray[j], maxWidth[j],
                                         ' ', !rightJust[j])
                                       : fieldArray[j])
                                    , false);
                        }

                        condlPrintln(LS + PRE_TR + "</TR>", true);
                        condlPrintln("", false);
                    }

                    condlPrintln("</TABLE>", true);

                    if (interactive && rows.size() != 1) {
                        stdprintln(LS + SqltoolRB.rows_fetched.getString(
                                rows.size()), true);
                    }

                    condlPrintln("<HR>", true);

                    break;
                }

                // STEP 2b (DSV)
                if (incCount > 0) {
                    for (int i = 0; i < headerArray.length; i++) {
                        dsvSafe(headerArray[i]);
                        pwDsv.print(headerArray[i]);

                        if (i < headerArray.length - 1) {
                            pwDsv.print(dsvColDelim);
                        }
                    }

                    pwDsv.print(dsvRowDelim);
                }

                for (String[] fArray : rows) {
                    for (int j = 0; j < fArray.length; j++) {
                        dsvSafe(fArray[j]);
                        pwDsv.print((fArray[j] == null)
                                    ? (autonulls[j] ? ""
                                                    : nullRepToken)
                                    : fArray[j]);

                        if (j < fArray.length - 1) {
                            pwDsv.print(dsvColDelim);
                        }
                    }

                    pwDsv.print(dsvRowDelim);
                }

                stdprintln(SqltoolRB.rows_fetched_dsv.getString(rows.size()));
                // Undecided about whether should display row count here when
                // in non-interactive mode
                break;

            default :
                shared.userVars.put("?", Integer.toString(updateCount));
                if (fetchingVar != null) {
                    shared.userVars.put(fetchingVar, shared.userVars.get("?"));
                    updateUserSettings();
                    fetchingVar = null;
                }

                if (updateCount != 0 && interactive) {
                    stdprintln((updateCount == 1)
                        ? SqltoolRB.row_update_singular.getString()
                        : SqltoolRB.row_update_multiple.getString(updateCount));
                }
                break;
        }
    }

    private static final int    COL_HEAD = 0,
                                COL_ODD  = 1,
                                COL_EVEN = 2
    ;
    private static final String PRE_TR   = "    ";
    private static final String PRE_TD   = "        ";

    /**
     * Print a properly formatted HTML &lt;TR&gt; command for the given
     * situation.
     *
     * @param colType Column type:  COL_HEAD, COL_ODD or COL_EVEN.
     */
    private static String htmlRow(int colType) {
        switch (colType) {
            case COL_HEAD :
                return PRE_TR + "<TR style='font-weight: bold;'>";

            case COL_ODD :
                return PRE_TR
                       + "<TR style='background: #94d6ef; font: normal "
                       + "normal 10px/10px Arial, Helvitica, sans-serif;'>";

            case COL_EVEN :
                return PRE_TR
                       + "<TR style='background: silver; font: normal "
                       + "normal 10px/10px Arial, Helvitica, sans-serif;'>";
        }

        return null;
    }

    /**
     * Returns a divider of hypens of requested length.
     *
     * @param len Length of output String.
     */
    private static String divider(int len) {
        return (len > DIVIDER.length()) ? DIVIDER
                                        : DIVIDER.substring(0, len);
    }

    /**
     * Display command history.
     */
    private void showHistory() throws BadSpecial {
        if (history == null) {
            throw new BadSpecial(SqltoolRB.history_unavailable.getString());
        }
        if (history.size() < 1) {
            throw new BadSpecial(SqltoolRB.history_none.getString());
        }
        if (shared.psStd == null) return;
          // Input can be dual-purpose, i.e. the script can be intended for
          // both interactive and non-interactive usage.
        Token token;
        for (int i = 0; i < history.size(); i++) {
            token = history.get(i);
            shared.psStd.println("#" + (i + oldestHist) + " or "
                    + (i - history.size()) + ':');
            shared.psStd.println(token.reconstitute());
        }
        if (buffer != null) {
            shared.psStd.println(SqltoolRB.editbuffer_contents.getString(
                    buffer.reconstitute()));
        }

        shared.psStd.println();
        shared.psStd.println(SqltoolRB.buffer_instructions.getString());
    }

    /**
     * Return a Command from command history.
     */
    private Token commandFromHistory(int inIndex) throws BadSpecial {
        int index = inIndex;  // Just to quiet compiler warnings.

        if (history == null) {
            throw new BadSpecial(SqltoolRB.history_unavailable.getString());
        }
        if (index == 0) {
            throw new BadSpecial(SqltoolRB.history_number_req.getString());
        }
        if (index > 0) {
            // Positive command# given
            index -= oldestHist;
            if (index < 0) {
                throw new BadSpecial(
                        SqltoolRB.history_backto.getString(oldestHist));
            }
            if (index >= history.size()) {
                throw new BadSpecial(SqltoolRB.history_upto.getString(
                       history.size() + oldestHist - 1));
            }
        } else {
            // Negative command# given
            index += history.size();
            if (index < 0) {
                throw new BadSpecial(
                        SqltoolRB.history_back.getString(history.size()));
            }
        }
        return history.get(index);
    }

    /**
     * Search Command History for a regex match.
     *
     * @return Absolute command number, if any match.
     */
    private Integer historySearch(String findRegex) throws BadSpecial {
        if (history == null) {
            throw new BadSpecial(SqltoolRB.history_unavailable.getString());
        }
        Pattern pattern = null;
        try {
            pattern = Pattern.compile("(?ims)" + findRegex);
        } catch (PatternSyntaxException pse) {
            throw new BadSpecial(
                    SqltoolRB.regex_malformat.getString(pse.getMessage()));
        }
        // Make matching more liberal.  Users can customize search behavior
        // by using "(?-OPTIONS)" or (?OPTIONS) in their regexes.
        for (int index = history.size() - 1; index >= 0; index--)
            if (pattern.matcher((history.get(index)).val).find())
                return Integer.valueOf(index + oldestHist);
        return null;
    }

    /**
     * Set buffer, unless the given token equals what is already in the
     * buffer.
     */
    private boolean setBuf(Token newBuffer) {
        if (buffer != null)
        if (buffer != null && buffer.equals(newBuffer)) return false;
        switch (newBuffer.type) {
            case Token.SQL_TYPE:
            case Token.PL_TYPE:
            case Token.SPECIAL_TYPE:
                break;
            default:
                throw new RuntimeException(
                        "Internal assertion failed.  "
                        + "Attempted to add command type "
                        + newBuffer.getTypeString() + " to buffer");
        }
        buffer = new Token(newBuffer.type, new String(newBuffer.val),
                newBuffer.line);
        // System.err.println("Buffer is now (" + buffer + ')');
        return true;
    }

    int oldestHist = 1;

    /**
     * Add a command onto the history list.
     */
    private boolean historize() {
        if (history == null || buffer == null) {
            return false;
        }
        if (history.size() > 0 &&
                history.get(history.size() - 1).equals(buffer)) {
            // Don't store two consecutive commands that are exactly the same.
            return false;
        }
        history.add(buffer);
        if (history.size() <= maxHistoryLength) {
            return true;
        }
        history.remove(0);
        oldestHist++;
        return true;
    }

    /**
     * Describe the columns of specified table.
     *
     * @param tableName  Table that will be described.
     * @param filter  Optional regex to filter by.
     *                By default, will match only against the column name.
     *                Prefix with "/" to match against the entire output line.
     */
    private void describe(String tableName,
                          String filterString) throws SQLException {
        if (shared.jdbcConn == null)
            throw new RuntimeException(
                    "Somehow got to 'describe' even though we have no Conn");
        /*
         * Doing case-sensitive filters now, for greater portability.
        String filter = ((inFilter == null) ? null : inFilter.toUpperCase());
         */
        Pattern   filter = null;
        boolean   filterMatchesAll = false// match filter against all cols.
        List<String[]> rows = new ArrayList<String[]>();
        String[]  headerArray = {
            SqltoolRB.describe_table_name.getString(),
            SqltoolRB.describe_table_datatype.getString(),
            SqltoolRB.describe_table_width.getString(),
            SqltoolRB.describe_table_nonulls.getString(),
        };
        String[]  fieldArray;
        int[]     maxWidth  = {
            0, 0, 0, 0
        };
        boolean[] rightJust = {
            false, false, true, false
        };

        if (filterString != null) try {
            filterMatchesAll = (filterString.charAt(0) == '/');
            filter = Pattern.compile(filterMatchesAll
                    ? filterString.substring(1) : filterString);
        } catch (PatternSyntaxException pse) {
            throw new SQLException(SqltoolRB.regex_malformat.getString(
                    pse.getMessage()));
            // This is obviously not a SQLException.
            // Perhaps change input parameter to a Pattern to require
            // caller to compile the pattern?
        }

        for (int i = 0; i < headerArray.length; i++) {
            if (htmlMode) {
                continue;
            }

            if (headerArray[i].length() > maxWidth[i]) {
                maxWidth[i] = headerArray[i].length();
            }
        }

        ResultSet r         = null;
        Statement statement = shared.jdbcConn.createStatement();

        // STEP 1: GATHER DATA
        try {
            statement.execute("SELECT * FROM " + tableName + " WHERE 1 = 2");

            r = statement.getResultSet();

            ResultSetMetaData m    = r.getMetaData();
            int               cols = m.getColumnCount();

            for (int i = 0; i < cols; i++) {
                fieldArray    = new String[4];
                fieldArray[0] = m.getColumnName(i + 1);

                if (filter != null && (!filterMatchesAll)
                        && !filter.matcher(fieldArray[0]).find()) {
                    continue;
                }

                fieldArray[1] = m.getColumnTypeName(i + 1);
                fieldArray[2] = Integer.toString(m.getColumnDisplaySize(i + 1));
                fieldArray[3] =
                    ((m.isNullable(i + 1) == java.sql.ResultSetMetaData.columnNullable)
                     ? (htmlMode ? "&nbsp;"
                                 : "")
                     : "*");

                if (filter != null && filterMatchesAll
                        && !filter.matcher(fieldArray[0]
                            + ' ' + fieldArray[1] + ' ' + fieldArray[2] + ' '
                            + fieldArray[3]).find()) {
                    continue;
                }

                rows.add(fieldArray);

                for (int j = 0; j < fieldArray.length; j++) {
                    if (fieldArray[j].length() > maxWidth[j]) {
                        maxWidth[j] = fieldArray[j].length();
                    }
                }
            }

            // STEP 2: DISPLAY DATA
            condlPrint("<TABLE border='1'>" + LS + SqlFile.htmlRow(COL_HEAD) + LS
                       + PRE_TD, true);

            for (int i = 0; i < headerArray.length; i++) {
                condlPrint("<TD>" + headerArray[i] + "</TD>", true);
                condlPrint(((i > 0) ? "  " : "")
                        + ((i < headerArray.length - 1 || rightJust[i])
                           ? StringUtil.toPaddedString(
                             headerArray[i], maxWidth[i], ' ', !rightJust[i])
                           : headerArray[i])
                        , false);
            }

            condlPrintln(LS + PRE_TR + "</TR>", true);
            condlPrintln("", false);

            if (!htmlMode) {
                for (int i = 0; i < headerArray.length; i++) {
                    condlPrint(((i > 0) ? "  "
                                        : "") + SqlFile.divider(maxWidth[i]), false);
                }

                condlPrintln("", false);
            }

            for (int i = 0; i < rows.size(); i++) {
                condlPrint(SqlFile.htmlRow(((i % 2) == 0) ? COL_EVEN
                                                  : COL_ODD) + LS
                                                  + PRE_TD, true);

                fieldArray = rows.get(i);

                for (int j = 0; j < fieldArray.length; j++) {
                    condlPrint("<TD>" + fieldArray[j] + "</TD>", true);
                    condlPrint(((j > 0) ? "  " : "")
                            + ((j < fieldArray.length - 1 || rightJust[j])
                               ? StringUtil.toPaddedString(
                                 fieldArray[j], maxWidth[j], ' ', !rightJust[j])
                               : fieldArray[j])
                            , false);
                }

                condlPrintln(LS + PRE_TR + "</TR>", true);
                condlPrintln("", false);
            }

            condlPrintln(LS + "</TABLE>" + LS + "<HR>", true);
        } finally {
            try {
                if (r != null) {
                    r.close();
                }

                statement.close();
            } catch (SQLException se) {
                // Purposefully doing nothing
            }
        }
    }

    private boolean eval(String[] inTokens) throws BadSpecial {
        /* TODO:  Rewrite using java.util.regex.  */
        // dereference *VARNAME variables.
        // N.b. we work with a "copy" of the tokens.
        boolean  negate = inTokens.length > 0 && inTokens[0].equals("!");
        String[] tokens = new String[negate ? (inTokens.length - 1)
                                            : inTokens.length];
        String inToken;

        for (int i = 0; i < tokens.length; i++) {
            inToken = inTokens[i + (negate ? 1 : 0)];
            if (inToken.length() > 1 && inToken.charAt(0) == '*') {
                tokens[i] = shared.userVars.get(inToken.substring(1));
            } else {
                tokens[i] = inTokens[i + (negate ? 1 : 0)];
            }

            // Unset variables permitted in expressions as long as use
            // the short *VARNAME form.
            if (tokens[i] == null) {
                tokens[i] = "";
            }
        }

        if (tokens.length == 1) {
            return (tokens[0].length() > 0 &&!tokens[0].equals("0")) ^ negate;
        }

        if (tokens.length == 3) {
            if (tokens[1].equals("==")) {
                return tokens[0].equals(tokens[2]) ^ negate;
            }

            if (tokens[1].equals("!=") || tokens[1].equals("<>")
                    || tokens[1].equals("><")) {
                return (!tokens[0].equals(tokens[2])) ^ negate;
            }

            if (tokens[1].equals(">")) {
                return (tokens[0].length() > tokens[2].length() || ((tokens[0].length() == tokens[2].length()) && tokens[0].compareTo(tokens[2]) > 0))
                       ^ negate;
            }

            if (tokens[1].equals("<")) {
                return (tokens[2].length() > tokens[0].length() || ((tokens[2].length() == tokens[0].length()) && tokens[2].compareTo(tokens[0]) > 0))
                       ^ negate;
            }
        }

        throw new BadSpecial(SqltoolRB.logical_unrecognized.getString());
    }

    private void closeQueryOutputStream() {
        if (pwQuery == null) {
            return;
        }

        try {
            if (htmlMode) {
                pwQuery.println("</BODY></HTML>");
                pwQuery.flush();
            }
        } finally {
            try {
                pwQuery.close();
            } finally {
                pwQuery = null; // Encourage GC of buffers
            }
        }
    }

    /**
     * Print to psStd and possibly pwQuery iff current HTML mode matches
     * supplied printHtml.
     */
    private void condlPrintln(String s, boolean printHtml) {
        if ((printHtml &&!htmlMode) || (htmlMode &&!printHtml)) {
            return;
        }

        if (shared.psStd != null) shared.psStd.println(s);

        if (pwQuery != null) {
            pwQuery.println(s);
            pwQuery.flush();
        }
    }

    /**
     * Print to psStd and possibly pwQuery iff current HTML mode matches
     * supplied printHtml.
     */
    private void condlPrint(String s, boolean printHtml) {
        if ((printHtml &&!htmlMode) || (htmlMode &&!printHtml)) {
            return;
        }

        if (shared.psStd != null) shared.psStd.print(s);

        if (pwQuery != null) {
            pwQuery.print(s);
            pwQuery.flush();
        }
    }

    private String formatNicely(Map<?, ?> map, boolean withValues) {
        String       s;
        StringBuffer sb = new StringBuffer();

        if (withValues) {
            SqlFile.appendLine(sb, SqltoolRB.pl_list_parens.getString());
        } else {
            SqlFile.appendLine(sb, SqltoolRB.pl_list_lengths.getString());
        }

        for (Map.Entry<Object, Object> entry
                : new TreeMap<Object, Object>(map).entrySet()) {
            s = (String) entry.getValue();

            SqlFile.appendLine(sb, "    " + (String) entry.getKey() + ": " + (withValues ? ("(" + s + ')')
                                                        : Integer.toString(
                                                        s.length())));
        }

        return sb.toString();
    }

    /**
     * Ascii file dump.
     *
     * dumpFile must not be null.
     */
    private void dump(String varName,
                      File dumpFile) throws IOException, BadSpecial {
        String val = shared.userVars.get(varName);

        if (val == null) {
            throw new BadSpecial(SqltoolRB.plvar_undefined.getString(varName));
        }

        OutputStreamWriter osw = new OutputStreamWriter(
                new FileOutputStream(dumpFile), (shared.encoding == null)
                ? DEFAULT_FILE_ENCODING : shared.encoding);

        try {
            osw.write(val);

            if (val.length() > 0) {
                char lastChar = val.charAt(val.length() - 1);

                if (lastChar != '\n' && lastChar != '\r') {
                    osw.write(LS);
                }
            }

            osw.flush();
        } finally {
            try {
                osw.close();
            } catch (IOException ioe) {
                // Intentionally empty
            } finally {
                osw = null// Encourage GC of buffers
            }
        }

        // Since opened in overwrite mode, since we didn't exception out,
        // we can be confident that we wrote all the bytest in the file.
        stdprintln(SqltoolRB.file_wrotechars.getString(
                Long.toString(dumpFile.length()), dumpFile.toString()));
    }

    byte[] binBuffer;

    /**
     * Binary file dump
     *
     * dumpFile must not be null.
     */
    private void dump(File dumpFile) throws IOException, BadSpecial {
        if (binBuffer == null) {
            throw new BadSpecial(SqltoolRB.binbuffer_empty.getString());
        }

        int len = 0;
        FileOutputStream fos = new FileOutputStream(dumpFile);

        try {
            fos.write(binBuffer);

            len = binBuffer.length;

            binBuffer = null;

            fos.flush();
        } finally {
            try {
                fos.close();
            } catch (IOException ioe) {
                // Intentionally empty
            } finally {
                fos = null; // Encourage GC of buffers
            }
        }
        stdprintln(SqltoolRB.file_wrotechars.getString(
                len, dumpFile.toString()));
    }

    /**
     * As the name says...
     * This method always closes the input stream.
     */
    public String streamToString(InputStream isIn, String cs)
            throws IOException {
        InputStream is = isIn;  // Compiler warning, when we can null the ref
        byte[] ba = null;
        int bytesread = 0;
        int retval;
        try {
            try {
                ba = new byte[is.available()];
            } catch (RuntimeException re) {
                throw new IOException(SqltoolRB.read_toobig.getString());
            }
            while (bytesread < ba.length &&
                    (retval = is.read(
                            ba, bytesread, ba.length - bytesread)) > 0) {
                bytesread += retval;
            }
            if (bytesread != ba.length) {
                throw new IOException(
                        SqltoolRB.read_partial.getString(bytesread, ba.length));
            }
            try {
                return (cs == null) ? (new String(ba))
                                         : (new String(ba, cs));
            } catch (UnsupportedEncodingException uee) {
                throw new IOException(
                        SqltoolRB.encode_fail.getString(uee.getMessage()));
            } catch (RuntimeException re) {
                throw new IOException(SqltoolRB.read_convertfail.getString());
            }
        } finally {
            try {
                is.close();
            } catch (IOException ioe) {
                // intentionally empty
            } finally {
                is = null// Encourage GC of buffers
            }
        }
    }

    /**
     * Ascii file load.
     */
    private void load(String varName, File asciiFile, String cs)
            throws IOException {
        String string = streamToString(new FileInputStream(asciiFile), cs);
        // The streamToString() method ensures that the Stream gets closed
        shared.userVars.put(varName, string);
        updateUserSettings();
    }

    /**
     * As the name says...
     */
    public static byte[] streamToBytes(InputStream is) throws IOException {
        byte[]                xferBuffer = new byte[10240];
        byte[]                outBytes = null;
        int                   i;
        ByteArrayOutputStream baos       = new ByteArrayOutputStream();

        try {
            while ((i = is.read(xferBuffer)) > 0) {
                baos.write(xferBuffer, 0, i);
            }
            outBytes = baos.toByteArray();
        } finally {
            baos = null// Encourage buffer GC
        }
        return outBytes;
    }

    /**
     * Binary file load
     *
     * @return The bytes which are the content of the fil
     */
    public static byte[] loadBinary(File binFile) throws IOException {
        byte[]                xferBuffer = new byte[10240];
        byte[]                outBytes = null;
        ByteArrayOutputStream baos;
        int                   i;
        FileInputStream       fis        = new FileInputStream(binFile);

        try {
            baos = new ByteArrayOutputStream();
            while ((i = fis.read(xferBuffer)) > 0) {
                baos.write(xferBuffer, 0, i);
            }
            outBytes = baos.toByteArray();
        } finally {
            try {
                fis.close();
            } catch (IOException ioe) {
                // intentionally empty
            } finally {
                fis = null; // Encourage GC of buffers
                baos = null; // Encourage GC of buffers
            }
        }

        return outBytes;
    }

    /**
     * This method is used to tell SqlFile whether this Sql Type must
     * ALWAYS be loaded to the binary buffer without displaying.
     * <P>
     * N.b.:  If this returns "true" for a type, then the user can never
     * "see" values for these columns.
     * Therefore, if a type may-or-may-not-be displayable, better to return
     * false here and let the user choose.
     * In general, if there is a toString() operator for this Sql Type
     * then return false, since the JDBC driver should know how to make the
     * value displayable.
     * </P>
     *
     * @see <A href="http://java.sun.com/docs/books/tutorial/jdbc/basics/retrieving.html">http://java.sun.com/docs/books/tutorial/jdbc/basics/retrieving.html</A>
     *      The table on this page lists the most common SqlTypes, all of which
     *      must implement toString()
     * @see java.sql.Types
     */
    public static boolean canDisplayType(int i) {
        /* I don't now about some of the more obscure types, like REF and
         * DATALINK */
        switch (i) {
            //case java.sql.Types.BINARY :
            case java.sql.Types.BLOB :
            case java.sql.Types.JAVA_OBJECT :

            //case java.sql.Types.LONGVARBINARY :
            //case java.sql.Types.LONGVARCHAR :
            case java.sql.Types.OTHER :
            case java.sql.Types.STRUCT :

                //case java.sql.Types.VARBINARY :
                return false;
        }

        return true;
    }

    // won't compile with JDK 1.4 without these
    private static final int JDBC3_BOOLEAN  = 16;
    private static final int JDBC3_DATALINK = 70;

    /**
     * Return a String representation of the specified java.sql.Types type.
     */
    public static String sqlTypeToString(int i) {
        switch (i) {
            case java.sql.Types.ARRAY :
                return "ARRAY";

            case java.sql.Types.BIGINT :
                return "BIGINT";

            case java.sql.Types.BINARY :
                return "BINARY";

            case java.sql.Types.BIT :
                return "BIT";

            case java.sql.Types.BLOB :
                return "BLOB";

            case JDBC3_BOOLEAN :
                return "BOOLEAN";

            case java.sql.Types.CHAR :
                return "CHAR";

            case java.sql.Types.CLOB :
                return "CLOB";

            case JDBC3_DATALINK :
                return "DATALINK";

            case java.sql.Types.DATE :
                return "DATE";

            case java.sql.Types.DECIMAL :
                return "DECIMAL";

            case java.sql.Types.DISTINCT :
                return "DISTINCT";

            case java.sql.Types.DOUBLE :
                return "DOUBLE";

            case java.sql.Types.FLOAT :
                return "FLOAT";

            case java.sql.Types.INTEGER :
                return "INTEGER";

            case java.sql.Types.JAVA_OBJECT :
                return "JAVA_OBJECT";

            case java.sql.Types.LONGVARBINARY :
                return "LONGVARBINARY";

            case java.sql.Types.LONGVARCHAR :
                return "LONGVARCHAR";

            case java.sql.Types.NULL :
                return "NULL";

            case java.sql.Types.NUMERIC :
                return "NUMERIC";

            case java.sql.Types.OTHER :
                return "OTHER";

            case java.sql.Types.REAL :
                return "REAL";

            case java.sql.Types.REF :
                return "REF";

            case java.sql.Types.SMALLINT :
                return "SMALLINT";

            case java.sql.Types.STRUCT :
                return "STRUCT";

            case java.sql.Types.TIME :
                return "TIME";

            case java.sql.Types.TIMESTAMP :
                return "TIMESTAMP";

            case java.sql.Types.TINYINT :
                return "TINYINT";

            case java.sql.Types.VARBINARY :
                return "VARBINARY";

            case java.sql.Types.VARCHAR :
                return "VARCHAR";

            case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE :
                return "SQL_TIME_WITH_TIME_ZONE";

            case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE :
                return "SQL_TIMESTAMP_WITH_TIME_ZONE";
        }

        return "Unknown type " + i;
    }

    /**
     * Validate that String is safe to write TO DSV file.
     *
     * @throws SqlToolError if validation fails.
     */
    public void dsvSafe(String s) throws SqlToolError {
        if (pwDsv == null || dsvColDelim == null || dsvRowDelim == null
                || nullRepToken == null) {
            throw new RuntimeException(
                "Assertion failed.  \n"
                + "dsvSafe called when DSV settings are incomplete");
        }

        if (s == null) {
            return;
        }

        if (s.indexOf(dsvColDelim) > 0) {
            throw new SqlToolError(
                    SqltoolRB.dsv_coldelim_present.getString(dsvColDelim));
        }

        if (s.indexOf(dsvRowDelim) > 0) {
            throw new SqlToolError(
                    SqltoolRB.dsv_rowdelim_present.getString(dsvRowDelim));
        }

        if (s.trim().equals(nullRepToken)) {
            // The trim() is to avoid the situation where the contents of a
            // field "looks like" the null-rep token.
            throw new SqlToolError(
                    SqltoolRB.dsv_nullrep_present.getString(nullRepToken));
        }
    }

    /**
     * Translates user-supplied escapes into the traditionaly corresponding
     * corresponding binary characters.
     *
     * Allowed sequences:
     * <UL>
     <LI>\0\d+   (an octal digit)
     *  <LI>\[0-9]\d*  (a decimal digit)
     *  <LI>\[Xx][0-9]{2}  (a hex digit)
     *  <LI>\n  Newline  (Ctrl-J)
     *  <LI>\r  Carriage return  (Ctrl-M)
     *  <LI>\t  Horizontal tab  (Ctrl-I)
     *  <LI>\f  Form feed  (Ctrl-L)
     * </UL>
     *
     * Java 1.4 String methods will make this into a 1 or 2 line task.
     */
    public static String convertEscapes(String inString) {
        if (inString == null) {
            return null;
        }
        return convertNumericEscapes(
                convertEscapes(convertEscapes(convertEscapes(convertEscapes(
                    convertEscapes(inString, "\\n", "\n"), "\\r", "\r"),
                "\\t", "\t"), "\\\\", "\\"),
            "\\f", "\f")
        );
    }

    /**
     * @param string  Non-null String to modify.
     */
    private static String convertNumericEscapes(String string) {
        String workString = string;
        int i = 0;

        for (char dig = '0'; dig <= '9'; dig++) {
            while ((i = workString.indexOf("\\" + dig, i)) > -1
                    && i < workString.length() - 1) {
                workString = convertNumericEscape(string, i);
            }
            while ((i = workString.indexOf("\\x" + dig, i)) > -1
                    && i < workString.length() - 1) {
                workString = convertNumericEscape(string, i);
            }
            while ((i = workString.indexOf("\\X" + dig, i)) > -1
                    && i < workString.length() - 1) {
                workString = convertNumericEscape(string, i);
            }
        }
        return workString;
    }

    /**
     * @offset  Position of the leading \.
     */
    private static String convertNumericEscape(String string, int offset) {
        int post = -1;
        int firstDigit = -1;
        int radix = -1;
        if (Character.toUpperCase(string.charAt(offset + 1)) == 'X') {
            firstDigit = offset + 2;
            radix = 16;
            post = firstDigit + 2;
            if (post > string.length()) post = string.length();
        } else {
            firstDigit = offset + 1;
            radix = (Character.toUpperCase(string.charAt(firstDigit)) == '0')
                    ? 8 : 10;
            post = firstDigit + 1;
            while (post < string.length()
                    && Character.isDigit(string.charAt(post))) post++;
        }
        return string.substring(0, offset) + ((char)
                Integer.parseInt(string.substring(firstDigit, post), radix))
                + string.substring(post);
    }

    /**
     * @param string  Non-null String to modify.
     */
    private static String convertEscapes(String string, String from, String to) {
        String workString = string;
        int i = 0;
        int fromLen = from.length();

        while ((i = workString.indexOf(from, i)) > -1
                && i < workString.length() - 1) {
            workString = workString.substring(0, i) + to
                         + workString.substring(i + fromLen);
        }
        return workString;
    }

    /**
     * Name is self-explanatory.
     *
     * If there is user demand, open file in random access mode so don't
     * need to load 2 copies of the entire file into memory.
     * This will be difficult because can't use standard Java language
     * features to search through a character array for multi-character
     * substrings.
     *
     * @throws SqlToolError  Would prefer to throw an internal exception,
     *                       but we want this method to have external
     *                       visibility.
     */
    public void importDsv(String filePath, String skipPrefix)
            throws SqlToolError {
        requireConnection();
        /* To make string comparisons, contains() methods, etc. a little
         * simpler and concise, just switch all column names to lower-case.
         * This is ok since we acknowledge up front that DSV import/export
         * assume no special characters or escaping in column names. */
        Matcher matcher;
        byte[] bfr  = null;
        File   dsvFile = new File(filePath);
        SortedMap<String, String> constColMap = null;
        if (dsvConstCols != null) {
            // We trim col. names, but not values.  Must allow users to
            // specify values as spaces, empty string, null.
            constColMap = new TreeMap<String, String>();
            for (String constPair : dsvConstCols.split(dsvColSplitter, -1)) {
                matcher = nameValPairPattern.matcher(constPair);
                if (!matcher.matches()) {
                    throw new SqlToolError(
                            SqltoolRB.dsv_constcols_nullcol.getString());
                }
                constColMap.put(matcher.group(1).toLowerCase(),
                        ((matcher.groupCount() < 2 || matcher.group(2) == null)
                        ? "" : matcher.group(2)));
            }
        }
        Set<String> skipCols = null;
        if (dsvSkipCols != null) {
            skipCols = new HashSet<String>();
            for (String skipCol : dsvSkipCols.split(dsvColSplitter, -1)) {
                skipCols.add(skipCol.trim().toLowerCase());
            }
        }

        if (!dsvFile.canRead()) {
            throw new SqlToolError(SqltoolRB.file_readfail.getString(
                    dsvFile.toString()));
        }

        try {
            bfr = new byte[(int) dsvFile.length()];
        } catch (RuntimeException re) {
            throw new SqlToolError(SqltoolRB.read_toobig.getString(), re);
        }

        int bytesread = 0;
        int retval;
        InputStream is = null;

        try {
            is = new FileInputStream(dsvFile);
            while (bytesread < bfr.length &&
                    (retval = is.read(bfr, bytesread, bfr.length - bytesread))
                    > 0) {
                bytesread += retval;
            }

        } catch (IOException ioe) {
            throw new SqlToolError(ioe);
        } finally {
            if (is != null) try {
                is.close();
            } catch (IOException ioe) {
                errprintln(
                        SqltoolRB.inputfile_closefail.getString() + ": " + ioe);
            } finally {
                is = null// Encourage GC of buffers
            }
        }
        if (bytesread != bfr.length) {
            throw new SqlToolError(SqltoolRB.read_partial.getString(
                    bytesread, bfr.length));
        }

        String dateString;
        String[] lines = null;

        try {
            String string = new String(bfr, (shared.encoding == null)
                    ? DEFAULT_FILE_ENCODING : shared.encoding);
            lines = string.split(dsvRowSplitter, -1);
        } catch (UnsupportedEncodingException uee) {
            throw new SqlToolError(uee);
        } catch (RuntimeException re) {
            throw new SqlToolError(SqltoolRB.read_convertfail.getString(), re);
        }

        List<String> headerList = new ArrayList<String>();
        String    tableName = dsvTargetTable;

        // First read one until we get one header line
        int lineCount = 0;
        String trimmedLine = null;
        boolean switching = false;
        int headerOffset = 0//  Used to offset read-start of header record
        String curLine = "dummy"; // Val will be replaced 4 lines down
                                  // This is just to quiet compiler warning

        while (true) {
            if (lineCount >= lines.length)
                throw new SqlToolError(SqltoolRB.dsv_header_none.getString());
            curLine = lines[lineCount++];
            trimmedLine = curLine.trim();
            if (trimmedLine.length() < 1
                    || (skipPrefix != null
                            && trimmedLine.startsWith(skipPrefix))) {
                continue;
            }
            if (trimmedLine.startsWith("targettable=")) {
                if (tableName == null) {
                    tableName = trimmedLine.substring(
                            "targettable=".length()).trim();
                }
                continue;
            }
            if (trimmedLine.equals("headerswitch{")) {
                if (tableName == null) {
                    throw new SqlToolError(
                            SqltoolRB.dsv_header_noswitchtarg.getString(
                            lineCount));
                }
                switching = true;
                continue;
            }
            if (trimmedLine.equals("}")) {
                throw new SqlToolError(
                        SqltoolRB.dsv_header_noswitchmatch.getString(lineCount));
            }
            if (!switching) {
                break;
            }
            int colonAt = trimmedLine.indexOf(':');
            if (colonAt < 1 || colonAt == trimmedLine.length() - 1) {
                throw new SqlToolError(
                        SqltoolRB.dsv_header_nonswitched.getString(lineCount));
            }
            String headerName = trimmedLine.substring(0, colonAt).trim();
            // Need to be sure here that tableName is not null (in
            // which case it would be determined later on by the file name).
            if (headerName.equals("*")
                    || headerName.equalsIgnoreCase(tableName)){
                headerOffset = 1 + curLine.indexOf(':');
                break;
            }
            // Skip non-matched header line
        }

        String headerLine = curLine.substring(headerOffset);
        String colName;
        String[] cols = headerLine.split(dsvColSplitter, -1);

        for (String col : cols) {
            if (col.length() < 1) {
                throw new SqlToolError(SqltoolRB.dsv_nocolheader.getString(
                        headerList.size() + 1, lineCount));
            }

            colName = col.trim().toLowerCase();
            headerList.add(
                (colName.equals("-")
                        || (skipCols != null
                                && skipCols.remove(colName))
                        || (constColMap != null
                                && constColMap.containsKey(colName))
                )
                ? ((String) null)
                : colName);
        }
        if (skipCols != null && skipCols.size() > 0) {
            throw new SqlToolError(SqltoolRB.dsv_skipcols_missing.getString(
                    skipCols.toString()));
        }

        boolean oneCol = false// At least 1 non-null column
        for (String header : headerList) {
            if (header != null) {
                oneCol = true;
                break;
            }
        }
        if (oneCol == false) {
            // Difficult call, but I think in any real-world situation, the
            // user will want to know if they are inserting records with no
            // data from their input file.
            throw new SqlToolError(
                    SqltoolRB.dsv_nocolsleft.getString(dsvSkipCols));
        }

        int inputColHeadCount = headerList.size();

        if (constColMap != null) {
            headerList.addAll(constColMap.keySet());
        }

        String[]  headers   = headerList.toArray(new String[0]);
        // headers contains input headers + all constCols, some of these
        // values may be nulls.

        if (tableName == null) {
            tableName = dsvFile.getName();

            int i = tableName.lastIndexOf('.');

            if (i > 0) {
                tableName = tableName.substring(0, i);
            }
        }

        StringBuffer tmpSb = new StringBuffer();
        List<String> tmpList = new ArrayList<String>();

        int skippers = 0;
        for (String header : headers) {
            if (header == null) {
                skippers++;
                continue;
            }
            if (tmpSb.length() > 0) {
                tmpSb.append(", ");
            }

            tmpSb.append(header);
            tmpList.add(header);
        }
        boolean[] autonulls = new boolean[headers.length - skippers];
        boolean[] parseDate = new boolean[autonulls.length];
        boolean[] parseBool = new boolean[autonulls.length];
        char[] readFormat = new char[autonulls.length];
        String[] insertFieldName = tmpList.toArray(new String[] {});
        // Remember that the headers array has all columns in DSV file,
        // even skipped columns.
        // The autonulls array only has columns that we will insert into.

        StringBuffer sb = new StringBuffer("INSERT INTO " + tableName + " ("
                                           + tmpSb + ") VALUES (");
        StringBuffer typeQuerySb = new StringBuffer("SELECT " + tmpSb
            + " FROM " + tableName + " WHERE 1 = 2");

        try {
            ResultSetMetaData rsmd =
                    shared.jdbcConn.createStatement().executeQuery(
                    typeQuerySb.toString()).getMetaData();

            if (rsmd.getColumnCount() != autonulls.length) {
                throw new SqlToolError(
                        SqltoolRB.dsv_metadata_mismatch.getString());
                // Don't know if it's possible to get here.
                // If so, it's probably a SqlTool problem, not a user or
                // data problem.
                // Should be researched and either return a user-friendly
                // message or a RuntimeExceptin.
            }

            for (int i = 0; i < autonulls.length; i++) {
                autonulls[i] = true;
                parseDate[i] = false;
                parseBool[i] = false;
                readFormat[i] = 's'; // regular Strings
                switch(rsmd.getColumnType(i + 1)) {
                    case java.sql.Types.BIT :
                        autonulls[i] = true;
                        readFormat[i] = 'b';
                        break;
                    case java.sql.Types.LONGVARBINARY :
                    case java.sql.Types.VARBINARY :
                    case java.sql.Types.BINARY :
                        autonulls[i] = true;
                        readFormat[i] = 'x';
                        break;
                    case java.sql.Types.BOOLEAN:
                        parseBool[i] = true;
                        break;
                    case java.sql.Types.ARRAY :
                        autonulls[i] = true;
                        readFormat[i] = 'a';
                        break;
                    case java.sql.Types.VARCHAR :
                    case java.sql.Types.BLOB :
                    case java.sql.Types.CLOB :
                    case java.sql.Types.LONGVARCHAR :
                        autonulls[i] = false;
                        // This means to preserve white space and to insert
                        // "" for "".  Otherwise we trim white space and
                        // insert null for \s*.
                        break;
                    case java.sql.Types.DATE:
                    case java.sql.Types.TIME:
                    case java.sql.Types.TIMESTAMP:
                    case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE:
                    case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE:
                        parseDate[i] = true;
                }
            }
        } catch (SQLException se) {
            throw new SqlToolError(SqltoolRB.query_metadatafail.getString(
                    typeQuerySb.toString()), se);
        }

        for (int i = 0; i < autonulls.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }

            sb.append('?');
        }

        // Initialize REJECT file(s)
        int rejectCount = 0;
        File rejectFile = null;
        File rejectReportFile = null;
        PrintWriter rejectWriter = null;
        PrintWriter rejectReportWriter = null;
        try {
        if (dsvRejectFile != null) try {
            rejectFile = new File(dsvRejectFile);
            rejectWriter = new PrintWriter(
                    new OutputStreamWriter(new FileOutputStream(rejectFile),
                    (shared.encoding == null)
                    ? DEFAULT_FILE_ENCODING : shared.encoding));
            rejectWriter.print(headerLine + dsvRowDelim);
        } catch (IOException ioe) {
            throw new SqlToolError(SqltoolRB.dsv_rejectfile_setupfail.getString(
                    dsvRejectFile), ioe);
        }
        if (dsvRejectReport != null) try {
            rejectReportFile = new File(dsvRejectReport);
            rejectReportWriter = new PrintWriter(new OutputStreamWriter(
                    new FileOutputStream(rejectReportFile),
                    (shared.encoding == null)
                    ? DEFAULT_FILE_ENCODING : shared.encoding));
            rejectReportWriter.println(SqltoolRB.rejectreport_top.getString(
                    (new java.util.Date()).toString(),
                    dsvFile.getPath(),
                    ((rejectFile == null) ? SqltoolRB.none.getString()
                                    : rejectFile.getPath()),
                    ((rejectFile == null) ? null : rejectFile.getPath())));
        } catch (IOException ioe) {
            throw new SqlToolError(
                    SqltoolRB.dsv_rejectreport_setupfail.getString(
                    dsvRejectReport), ioe);
        }

        int recCount = 0;
        int skipCount = 0;
        PreparedStatement ps = null;
        boolean importAborted = false;
        boolean doResetAutocommit = false;
        try {
            doResetAutocommit = dsvRecordsPerCommit > 0
                && shared.jdbcConn.getAutoCommit();
            if (doResetAutocommit) shared.jdbcConn.setAutoCommit(false);
        } catch (SQLException se) {
            throw new SqlToolError(
                    SqltoolRB.rpc_autocommit_failure.getString(), se);
        }
        // We're now assured that if dsvRecordsPerCommit is > 0, then
        // autocommit is off.

        try {
            try {
                ps = shared.jdbcConn.prepareStatement(sb.toString() + ')');
            } catch (SQLException se) {
                throw new SqlToolError(SqltoolRB.insertion_preparefail.getString(
                        sb.toString()), se);
            }
            String[] dataVals = new String[autonulls.length];
            // Length is number of cols to insert INTO, not nec. # in DSV file.
            int      readColCount;
            int      storeColCount;
            Matcher  arMatcher;
            String   currentFieldName = null;
            String[] arVals;

            // Insert data rows 1-row-at-a-time
            while (lineCount < lines.length) try { try {
                curLine = lines[lineCount++];
                trimmedLine = curLine.trim();
                if (trimmedLine.length() < 1) {
                    continue// Silently skip blank lines
                }
                if (skipPrefix != null
                        && trimmedLine.startsWith(skipPrefix)) {
                    skipCount++;
                    continue;
                }
                if (switching) {
                    if (trimmedLine.equals("}")) {
                        switching = false;
                        continue;
                    }
                    int colonAt = trimmedLine.indexOf(':');
                    if (colonAt < 1 || colonAt == trimmedLine.length() - 1) {
                        throw new SqlToolError(SqltoolRB.dsv_header_matchernonhead.getString(
                                        lineCount));
                    }
                    continue;
                }
                // Finished using "trimmed" line now.  Whitespace is
                // meaningful hereafter.

                // Finally we will attempt to add a record!
                recCount++;
                // Remember that recCount counts both inserts + rejects

                readColCount = 0;
                storeColCount = 0;
                cols = curLine.split(dsvColSplitter, -1);

                for (String col : cols) {
                    if (readColCount == inputColHeadCount) {
                        throw new RowError(SqltoolRB.dsv_colcount_mismatch.getString(
                                inputColHeadCount, 1 + readColCount));
                    }

                    if (headers[readColCount++] != null) {
                        dataVals[storeColCount++] = dsvTrimAll ? col.trim() : col;
                    }
                }
                if (readColCount < inputColHeadCount) {
                    throw new RowError(SqltoolRB.dsv_colcount_mismatch.getString(
                            inputColHeadCount, readColCount));
                }
                /* Already checked for readColCount too high in prev. block */

                if (constColMap != null) {
                    for (String val : constColMap.values()) {
                        dataVals[storeColCount++] = val;
                    }
                }
                if (storeColCount != dataVals.length) {
                    throw new RowError(SqltoolRB.dsv_insertcol_mismatch.getString(
                            dataVals.length, storeColCount));
                }

                for (int i = 0; i < dataVals.length; i++) {
                    currentFieldName = insertFieldName[i];
                    if (autonulls[i]) dataVals[i] = dataVals[i].trim();
                    // N.b. WE SPECIFICALLY DO NOT HANDLE TIMES WITHOUT
                    // DATES, LIKE "3:14:00", BECAUSE, WHILE THIS MAY BE
                    // USEFUL AND EFFICIENT, IT IS NOT PORTABLE.
                    //System.err.println("ps.setString(" + i + ", "
                    //      + dataVals[i] + ')');

                    if (parseDate[i]) {
                        if ((dataVals[i].length() < 1 && autonulls[i])
                              || dataVals[i].equals(nullRepToken)) {
                            ps.setTimestamp(i + 1, null);
                        } else {
                            dateString = (dataVals[i].indexOf(':') > 0)
                                       ? dataVals[i]
                                       : (dataVals[i] + " 0:00:00");
                            // BEWARE:  This may not work for some foreign
                            // date/time formats.
                            try {
                                ps.setTimestamp(i + 1,
                                        java.sql.Timestamp.valueOf(dateString));
                            } catch (IllegalArgumentException iae) {
                                throw new RowError(SqltoolRB.time_bad.getString(
                                        dateString), iae);
                            }
                        }
                    } else if (parseBool[i]) {
                        if ((dataVals[i].length() < 1 && autonulls[i])
                              || dataVals[i].equals(nullRepToken)) {
                            ps.setNull(i + 1, java.sql.Types.BOOLEAN);
                        } else {
                            try {
                                ps.setBoolean(i + 1,
                                        Boolean.parseBoolean(dataVals[i]));
                                // Boolean... is equivalent to Java 4's
                                // Boolean.parseBoolean().
                            } catch (IllegalArgumentException iae) {
                                throw new RowError(SqltoolRB.boolean_bad.getString(
                                        dataVals[i]), iae);
                            }
                        }
                    } else {
                        switch (readFormat[i]) {
                            case 'b':
                                ps.setBytes(
                                    i + 1,
                                    (dataVals[i].length() < 1) ? null
                                    : SqlFile.bitCharsToBytes(
                                        dataVals[i]));
                                break;
                            case 'x':
                                ps.setBytes(
                                    i + 1,
                                    (dataVals[i].length() < 1) ? null
                                    : SqlFile.hexCharOctetsToBytes(
                                        dataVals[i]));
                                break;
                            case 'a' :
                                if (SqlFile.createArrayOfMethod == null) {
                                    throw new SqlToolError(
                                            //SqltoolRB.boolean_bad.getString(
                                        "SqlTool requires += Java 1.6 at "
                                        + "runtime in order to import Array "
                                        + "values");
                                }
                                if (dataVals[i].length() < 1) {
                                    ps.setArray(i + 1, null);
                                    break;
                                }
                                arMatcher = arrayPattern.matcher(dataVals[i]);
                                if (!arMatcher.matches()) {
                                    throw new RowError(
                                            //SqltoolRB.boolean_bad.getString(
                                        "Malformatted ARRAY value: ("
                                        + dataVals[i] + ')');
                                }
                                arVals = (arMatcher.group(1) == null)
                                       ? (new String[0])
                                       : arMatcher.group(1).split("\\s*,\\s*");
                                // N.b. THIS DOES NOT HANDLE commas WITHIN
                                // Array ELEMENT VALUES.
                                try {
                                    ps.setArray(i + 1, (java.sql.Array)
                                            SqlFile.createArrayOfMethod.invoke(
                                            shared.jdbcConn,
                                            "VARCHAR", arVals));
                                } catch (IllegalAccessException iae) {
                                    throw new RuntimeException(iae);
                                } catch (InvocationTargetException ite) {
                                    if (ite.getCause() != null
                                            &&  ite.getCause()
                                            instanceof AbstractMethodError) {
                                        throw new SqlToolError(
                                            //SqltoolRB.boolean_bad.getString(
                                            "SqlTool binary is not "
                                            + "Array-compatible with your "
                                            + "runtime JRE.  Array imports "
                                            + "not possible.");
                                    }
                                    throw new RuntimeException(ite);
                                }
                                // createArrayOf method is Java-6-specific!
                                break;
                            default:
                                ps.setString(
                                    i + 1,
                                    (((dataVals[i].length() < 1 && autonulls[i])
                                      || dataVals[i].equals(nullRepToken))
                                     ? null
                                     : dataVals[i]));
                        }
                    }
                    currentFieldName = null;
                }

                retval = ps.executeUpdate();

                if (retval != 1) {
                    throw new RowError(
                            SqltoolRB.inputrec_modified.getString(retval));
                }

                if (dsvRecordsPerCommit > 0
                    && (recCount - rejectCount) % dsvRecordsPerCommit == 0) {
                    shared.jdbcConn.commit();
                    shared.possiblyUncommitteds = false;
                } else {
                    shared.possiblyUncommitteds = true;
                }
            } catch (NumberFormatException nfe) {
                throw new RowError(null, nfe);
            } catch (SQLException se) {
                throw new RowError(null, se);
            } } catch (RowError re) {
                rejectCount++;
                if (rejectWriter != null || rejectReportWriter != null) {
                    if (rejectWriter != null) {
                        rejectWriter.print(curLine + dsvRowDelim);
                    }
                    if (rejectReportWriter != null) {
                        genRejectReportRecord(rejectReportWriter,
                                rejectCount, lineCount,
                                currentFieldName, re.getMessage(),
                                re.getCause());
                    }
                } else {
                    importAborted = true;
                    throw new SqlToolError(
                            SqltoolRB.dsv_recin_fail.getString(
                                    lineCount, currentFieldName)
                            + ((re.getMessage() == null)
                                    ? "" : ("  " + re.getMessage())),
                            re.getCause());
                }
            }
        } finally {
            if (ps != null) try {
                ps.close();
            } catch (SQLException se) {
                // We already got what we want from it, or have/are
                // processing a more specific error.
            } finally {
                ps = null// Encourage GC of buffers
            }
            try {
                if (dsvRecordsPerCommit > 0
                    && (recCount - rejectCount) % dsvRecordsPerCommit != 0) {
                    // To be consistent, if *DSV_RECORDS_PER_COMMIT is set, we
                    // always commit all inserted records.
                    // This little block commits any straggler commits since the
                    // last commit.
                    shared.jdbcConn.commit();
                    shared.possiblyUncommitteds = false;
                }
                if (doResetAutocommit) shared.jdbcConn.setAutoCommit(true);
            } catch (SQLException se) {
                throw new SqlToolError(
                        SqltoolRB.rpc_commit_failure.getString(), se);
            }
            String summaryString = null;
            if (recCount > 0) {
                summaryString = SqltoolRB.dsv_import_summary.getString(
                        ((skipPrefix == null)
                                  ? "" : ("'" + skipPrefix + "'-")),
                        Integer.toString(skipCount),
                        Integer.toString(rejectCount),
                        Integer.toString(recCount - rejectCount),
                        (importAborted ? "importAborted" : null));
                stdprintln(summaryString);
            }
            try {
                if (recCount > rejectCount && dsvRecordsPerCommit < 1
                        && !shared.jdbcConn.getAutoCommit()) {
                    stdprintln(SqltoolRB.insertions_notcommitted.getString());
                }
            } catch (SQLException se) {
                stdprintln(SqltoolRB.autocommit_fetchfail.getString());
                stdprintln(SqltoolRB.insertions_notcommitted.getString());
                // No reason to throw here.  If user attempts to use the
                // connection for anything significant, we will throw then.
            }
            if (rejectWriter != null) {
                rejectWriter.flush();
            }
            if (rejectReportWriter != null && rejectCount > 0) {
                rejectReportWriter.println(SqltoolRB.rejectreport_bottom.getString(
                        summaryString, revnum));
                rejectReportWriter.flush();
            }
            if (rejectCount == 0) {
                if (rejectFile != null && rejectFile.exists()
                        && !rejectFile.delete())
                    errprintln(SqltoolRB.dsv_rejectfile_purgefail.getString(
                            rejectFile.toString()));
                if (rejectReportFile != null && !rejectReportFile.delete())
                    errprintln(SqltoolRB.dsv_rejectreport_purgefail.getString(
                            (rejectFile == null)
                                    ? null : rejectFile.toString()));
                // These are trivial errors.
            }
        }
        } finally {
            if (rejectWriter != null) {
                try {
                    rejectWriter.close();
                } finally {
                    rejectWriter = null// Encourage GC of buffers
                }
            }
            if (rejectReportWriter != null) {
                try {
                    rejectReportWriter.close();
                } finally {
                    rejectReportWriter = null// Encourage GC of buffers
                }
            }
        }
    }

    protected static void appendLine(StringBuffer sb, String s) {
        sb.append(s + LS);
    }

    /**
     * Does a poor-man's parse of a MSDOS command line and parses it
     * into a WIndows cmd.exe invocation to approximate.
     */
    private static String[] genWinArgs(String monolithic) {
        List<String> list = new ArrayList<String>();
        list.add("cmd.exe");
        list.add("/y");
        list.add("/c");
        Matcher m = wincmdPattern.matcher(monolithic);
        while (m.find()) {
            for (int i = 1; i <= m.groupCount(); i++) {
                if (m.group(i) == null) continue;
                if (m.group(i).length() > 1 && m.group(i).charAt(0) == '"') {
                    list.add(m.group(i).substring(1, m.group(i).length() - 1));
                    continue;
                }
                list.addAll(Arrays.asList(m.group(i).split("\\s+", -1)));
            }
        }
        return list.toArray(new String[] {});
    }

    private void genRejectReportRecord(PrintWriter pw, int rCount,
            int lCount, String field, String eMsg, Throwable cause) {
        pw.println(SqltoolRB.rejectreport_row.getString(
                ((rCount % 2 == 0) ? "even" : "odd") + "row",
                Integer.toString(rCount),
                Integer.toString(lCount),
                ((field == null) ? "&nbsp;" : field),
                (((eMsg == null) ? "" : eMsg)
                        + ((eMsg == null || cause == null) ? "" : "<HR/>")
                        + ((cause == null) ? "" : (
                                (cause instanceof SQLException
                                        && cause.getMessage() != null)
                                    ? cause.getMessage()
                                    : cause.toString()
                                )
                        )
                )));
    }

    /**
     * Parses input into command tokens, but does not perform the commands
     * (unless you consider parsing blocks of nested commands to be
     * "performing" a command).
     *
     * Throws only if I/O error or EOF encountered before end of entire file
     * (encountered at any level of recursion).
     *
     * Exceptions thrown within this method percolate right up to the
     * external call (in scanpass), regardless of ContinueOnErr setting.
     * This is because it's impossible to know when to terminate blocks
     * if there is a parsing error.
     * Only a separate SqlFile invocation (incl. \i command) will cause
     * a seekTokenSource exception to be handled at a level other than
     * the very top.
     */
    private TokenList seekTokenSource(String nestingCommand)
            throws BadSpecial, IOException {
        Token token;
        TokenList newTS = new TokenList();
        Pattern endPattern = Pattern.compile("end\\s+" + nestingCommand);
        String subNestingCommand;

        while ((token = scanner.yylex()) != null) {
            if (token.type == Token.PL_TYPE
                    && endPattern.matcher(token.val).matches()) {
                return newTS;
            }
            subNestingCommand = nestingCommand(token);
            if (subNestingCommand != null) {
                token.nestedBlock = seekTokenSource(subNestingCommand);
            }
            newTS.add(token);
        }
        throw new BadSpecial(
                SqltoolRB.pl_block_unterminated.getString(nestingCommand));
    }

    /**
     * We want leading space to be trimmed.
     * Leading space should probably not be trimmed, but it is trimmed now
     * (by the Scanner).
     */
    private void processMacro(Token defToken) throws BadSpecial {
        Matcher matcher;
        Token macroToken;

        if (defToken.val.length() < 1) {
            throw new BadSpecial(SqltoolRB.macro_tip.getString());
        }
        switch (defToken.val.charAt(0)) {
            case '?':
                stdprintln(SqltoolRB.macro_help.getString());
                break;
            case '=':
                String defString = defToken.val;
                defString = defString.substring(1).trim();
                if (defString.length() < 1) {
                    for (Map.Entry<String, Token> entry
                            : shared.macros.entrySet()) {
                        stdprintln(entry.getKey() + " = "
                                + entry.getValue().reconstitute());
                    }
                    break;
                }

                int newType = -1;
                StringBuffer newVal = new StringBuffer();
                matcher = editMacroPattern.matcher(defString);
                if (matcher.matches()) {
                    if (buffer == null) {
                        stdprintln(nobufferYetString);
                        return;
                    }
                    newVal.append(buffer.val);
                    if (matcher.groupCount() > 1 && matcher.group(2) != null
                            && matcher.group(2).length() > 0)
                        newVal.append(matcher.group(2));
                    newType = buffer.type;
                } else {
                    matcher = spMacroPattern.matcher(defString);
                    if (matcher.matches()) {
                        newVal.append(matcher.group(3));
                        newType = (matcher.group(2).equals("*")
                                ?  Token.PL_TYPE : Token.SPECIAL_TYPE);
                    } else {
                        matcher = sqlMacroPattern.matcher(defString);
                        if (!matcher.matches())
                            throw new BadSpecial(
                                    SqltoolRB.macro_malformat.getString());
                        newVal.append(matcher.group(2));
                        newType = Token.SQL_TYPE;
                    }
                }
                if (newVal.length() < 1)
                    throw new BadSpecial(SqltoolRB.macrodef_empty.getString());
                if (newVal.charAt(newVal.length() - 1) == ';')
                    throw new BadSpecial(SqltoolRB.macrodef_semi.getString());
                shared.macros.put(matcher.group(1),
                        new Token(newType, newVal, defToken.line));
                break;
            default:
                matcher = useMacroPattern.matcher(defToken.val);
                if (!matcher.matches())
                    throw new BadSpecial(SqltoolRB.macro_malformat.getString());
                macroToken = shared.macros.get(matcher.group(1));
                if (macroToken == null)
                    throw new BadSpecial(SqltoolRB.macro_undefined.getString(
                            matcher.group(1)));
                setBuf(macroToken);
                buffer.line = defToken.line;
                if (matcher.groupCount() > 1 && matcher.group(2) != null
                        && matcher.group(2).length() > 0)
                    buffer.val += matcher.group(2);
                preempt = matcher.group(matcher.groupCount()).equals(";");
        }
    }

    /**
     * Convert a String to a byte array by interpreting every 2 characters as
     * an octal byte value.
     */
    public static byte[] hexCharOctetsToBytes(String hexChars) {
        int chars = hexChars.length();
        if (chars != (chars / 2) * 2) {
            throw new NumberFormatException("Hex character lists contains "
                + "an odd number of characters: " + chars);
        }
        byte[] ba = new byte[chars/2];
        int offset = 0;
        char c;
        int octet;
        for (int i = 0; i < chars; i++) {
            octet = 0;
            c = hexChars.charAt(i);
            if (c >= 'a' && c <= 'f') {
                octet += 10 + c - 'a';
            } else if (c >= 'A' && c <= 'F') {
                octet += 10 + c - 'A';
            } else if (c >= '0' && c <= '9') {
                octet += c - '0';
            } else {
                throw new NumberFormatException(
                    "Non-hex character in input at offset " + i + ": " + c);
            }
            octet = octet << 4;
            c = hexChars.charAt(++i);
            if (c >= 'a' && c <= 'f') {
                octet += 10 + c - 'a';
            } else if (c >= 'A' && c <= 'F') {
                octet += 10 + c - 'A';
            } else if (c >= '0' && c <= '9') {
                octet += c - '0';
            } else {
                throw new NumberFormatException(
                    "Non-hex character in input at offset " + i + ": " + c);
            }

            ba[offset++] = (byte) octet;
        }
        if (ba.length != offset) {
            throw new RuntimeException(
                    "Internal accounting problem.  Expected to fill buffer of "
                    + "size "+ ba.length + ", but wrote only " + offset
                    + " bytes");
        }
        return ba;
    }

    /**
     * Just a stub for now.
     */
    public static byte[] bitCharsToBytes(String hexChars) {
        if (hexChars == null) throw new NullPointerException();
        // To shut up compiler warn
        throw new NumberFormatException(
                "Sorry.  Bit exporting not supported yet");
    }

    private void requireConnection() throws SqlToolError {
        if (shared.jdbcConn == null)
            throw new SqlToolError(SqltoolRB.no_required_conn.getString());
    }

    /**
     * Returns a String report for the specified JDBC Connection.
     *
     * For databases with poor JDBC support, you won't get much detail.
     */
    public static String getBanner(Connection c) {
        try {
            DatabaseMetaData md = c.getMetaData();
            return (md == null)
                    ? null
                    : SqltoolRB.jdbc_established.getString(
                            md.getDatabaseProductName(),
                            md.getDatabaseProductVersion(),
                            md.getUserName(),
                                    (c.isReadOnly() ? "R/O " : "R/W ")
                                    + RCData.tiToString(
                                    c.getTransactionIsolation()));
        } catch (SQLException se) {
            return null;
        }
    }

    private void displayConnBanner() {
        String msg = (shared.jdbcConn == null)
                   ? SqltoolRB.disconnected_msg.getString()
                   : SqlFile.getBanner(shared.jdbcConn);
        stdprintln((msg == null)
                  ? SqltoolRB.connected_fallbackmsg.getString()
                  : msg);
    }
}
TOP

Related Classes of org.hsqldb.cmdline.SqlFile$BadSubst

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.