org.hsqldb.cmdline.SqlFile Maven / Gradle / Ivy
Show all versions of sqltool Show documentation
/* Copyright (c) 2001-2021, 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.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.hsqldb.cmdline.sqltool.Calculator;
import org.hsqldb.cmdline.sqltool.FileRecordReader;
import org.hsqldb.cmdline.sqltool.SqlFileScanner;
import org.hsqldb.cmdline.sqltool.Token;
import org.hsqldb.cmdline.sqltool.TokenList;
import org.hsqldb.cmdline.sqltool.TokenSource;
import org.hsqldb.cmdline.libclone.AppendableException;
import org.hsqldb.cmdline.libclone.FrameworkLogger;
import org.hsqldb.cmdline.utilclone.RCData;
/* $Id: SqlFile.java 6266 2021-01-25 16:08:06Z fredt $ */
/**
* 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.
*
* The file src/org/hsqldb/sample/SqlFileEmbedder.java
* in the HSQLDB distribution provides an example for using SqlFile to
* execute SQL files directly from your own Java classes.
*
* The complexities of passing userVars and macros maps are to facilitate
* strong scoping (among blocks and nested scripts).
*
* Some implementation comments and variable names use keywords based
* on the following definitions.
* - COMMAND = Statement || SpecialCommand || BufferCommand
*
- Statement = SQL statement like "SQL Statement;"
*
- SpecialCommand = Special Command like "\x arg..."
*
- BufferCommand = Editing/buffer command like ":s/this/that/"
*
*
* 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.
*
* 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.
*
* Buffer commands are unique to SQLFile. The ":" commands allow
* you to edit the buffer and to execute the buffer.
*
* \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).
*
* The process*() methods, other than processBuffHist() ALWAYS execute
* on "buffer", and expect it to contain the method specific prefix
* (if any).
*
* 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).
*
* @see
* The SqlTool chapter of the
* HyperSQL Utilities Guide
* @see org.hsqldb.sample.SqlFileEmbedder
* @version $Revision: 6266 $, $Date: 2021-01-25 11:08:06 -0500 (Mon, 25 Jan 2021) $
* @author Blaine Simpson (blaine dot simpson at admc dot com)
*/
public class SqlFile {
// copies of Java 8 additions to java.sql.Types constants
private static final int TIME_WITH_TIMEZONE = 2013;
private static final int TIMESTAMP_WITH_TIMEZONE = 2014;
private enum Recursion { FILE, IF, WHILE, FOREACH, FOR, FORROWS }
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;
/** Platform-specific line separator */
public static final 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;
private URL atBase;
private boolean dsvTrimAll;
private boolean ignoreBangStatus;
private boolean allQuoted;
private boolean doPrepare;
private static String DSV_X_SYNTAX_MSG;
private static String DSV_M_SYNTAX_MSG;
private static String nobufferYetString;
private String prepareVar;
private int dsvRecordsPerCommit = 0;
static String DEFAULT_FILE_ENCODING =
System.getProperty("file.encoding");
// These settings are never null
private String nullRepToken; // May be ""
private String nullRepHtml; // May be ""
private String dsvColDelim; // May NOT be ""
private String dsvColSplitter; // May NOT be ""
private String dsvRowDelim; // May NOT be ""
private String dsvRowSplitter; // May NOT be ""
private String dsvConstCols; // May NOT be ""
private String dsvSkipPrefix; // May not be ""
// Following may be null but not ""
private String dsvSkipCols;
private String dsvTargetFile;
private String dsvTargetTable;
private String dsvRejectFile;
private String dsvRejectReport;
private String topHtmlFile;
private String bottomHtmlFile;
private SimpleDateFormat timestampFormat;
/**
* 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 varPattern = Pattern.compile("\\*?[a-zA-Z]\\w*");
private static Pattern wordAndDotPattern = Pattern.compile("[\\w.]+");
private static Pattern specialPattern =
Pattern.compile("(\\S+)(?:\\s(.*))?");
private static Pattern plPattern = Pattern.compile("(.*\\S)?\\s*");
private static Pattern mathAsgnPattern = Pattern.compile(
"\\(\\(\\s*([a-zA-Z]\\w*)\\s*([-+*/%][-+=])\\s*(.+?)?\\s*\\)\\)\\s*");
private static Pattern mathPattern = Pattern.compile(
"\\(\\(\\s*([a-zA-Z]\\w*)\\s*=\\s*(.+?)?\\s*\\)\\)\\s*");
private static Pattern foreachPattern =
Pattern.compile("foreach\\s+(\\S+)\\s*\\(([^)]+)\\)\\s*");
private static Pattern forrowsPattern =
Pattern.compile("forrows((?:\\s+[a-zA-Z]\\w*)*)\\s*");
private static Pattern forPattern = Pattern.compile(
"for\\s+(\\(\\(.+\\)\\))?\\s*(\\([^)]+\\))\\s*(\\(\\(.+\\)\\))\\s*");
private static Pattern ifwhilePattern =
Pattern.compile("\\S+\\s*\\(([^)]*)\\)\\s*");
private static Pattern inlineifPattern =
Pattern.compile("(if\\s*\\([^)]*\\))(.*\\S.*)");
private static Pattern varsetPattern =
Pattern.compile("(\\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 useFnPattern =
Pattern.compile("(\\w+\\()\\s*([^;)]*?)\\s*\\)(.*)");
private static Pattern legacyEditMacroPattern =
Pattern.compile("(\\w+(?:\\(\\))?)\\s*:(.*)");
private static Pattern editMacroPattern =
Pattern.compile(":\\s(\\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 Pattern fnParamPat = Pattern.compile("\\*\\{(:)?(\\d+)\\}");
static final Pattern URL_WITH_PROTO_RE = Pattern.compile("[a-z]{2,}:.+");
private static Map nestingPLCommands =
new HashMap();
private static Map inlineNestPLCommands =
new HashMap();
static {
nestingPLCommands.put("if", ifwhilePattern);
nestingPLCommands.put("while", ifwhilePattern);
nestingPLCommands.put("foreach", foreachPattern);
nestingPLCommands.put("forrows", forrowsPattern);
nestingPLCommands.put("for", forPattern);
inlineNestPLCommands.put("if", inlineifPattern);
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.
private boolean removeEmptyVars() {
String sysP = System.getProperty("sqltool.REMOVE_EMPTY_VARS");
return sysP != null && Boolean.parseBoolean(sysP);
}
/**
* Encapsulate updating local variables which depend upon PL variables.
*
* 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.
* TODO: Could do what is needed by making a Map subclass with
* overridden setters which enforce dirtiness.
*/
private void updateUserSettings() {
// Unset those system userVars where empty string makes no sense.
String varVal;
if (shared.userVars.containsKey("NULL")
|| shared.userVars.containsKey("*NULL")) {
errprintln(SqltoolRB.null_assignment.getString());
shared.userVars.remove("NULL");
shared.userVars.remove("*NULL");
}
for (String noEmpty : new String[] {
"DSV_SKIP_COLS", "DSV_COL_DELIM", "TIMESTAMP_FORMAT",
"DSV_COL_SPLITTER", "DSV_ROW_DELIM", "DSV_ROW_SPLITTER",
"DSV_TARGET_FILE", "DSV_TARGET_TABLE", "DSV_CONST_COLS",
"DSV_REJECT_FILE", "DSV_REJECT_REPORT", "DSV_RECORDS_PER_COMMIT",
}) {
varVal = shared.userVars.get('*' + noEmpty);
if (varVal == null || varVal.length() > 0) {
continue;
}
if (!removeEmptyVars()) {
errprintln(SqltoolRB.auto_unset_warning.getString(noEmpty));
}
shared.userVars.remove('*' + noEmpty);
}
// Null/empty policy of *DSV_SKIP_PREFIX variable is very different
// from that of our dsvSkipPrefix local variable.
// *DSV... null -> dsv* default
// *DSV... "" -> dsv* null
// There is no dsv* of ""
dsvSkipPrefix = SqlFile.convertEscapes(
shared.userVars.get("*DSV_SKIP_PREFIX"));
if (dsvSkipPrefix == null) {
dsvSkipPrefix = DEFAULT_SKIP_PREFIX;
} else if (dsvSkipPrefix.length() < 1) {
dsvSkipPrefix = null;
}
dsvSkipCols = shared.userVars.get("*DSV_SKIP_COLS");
dsvTrimAll = Boolean.parseBoolean(
shared.userVars.get("*DSV_TRIM_ALL"));
ignoreBangStatus = Boolean.parseBoolean(
shared.userVars.get("*IGNORE_BANG_STATUS"));
allQuoted = Boolean.parseBoolean(
shared.userVars.get("*ALL_QUOTED"));
dsvColDelim = SqlFile.convertEscapes(
shared.userVars.get("*DSV_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 = DEFAULT_ROW_DELIM;
dsvRowSplitter = shared.userVars.get("*DSV_ROW_SPLITTER");
if (dsvRowSplitter == null) dsvRowSplitter = DEFAULT_ROW_SPLITTER;
dsvTargetFile = shared.userVars.get("*DSV_TARGET_FILE");
dsvTargetTable = shared.userVars.get("*DSV_TARGET_TABLE");
dsvConstCols = shared.userVars.get("*DSV_CONST_COLS");
dsvRejectFile = shared.userVars.get("*DSV_REJECT_FILE");
dsvRejectReport = shared.userVars.get("*DSV_REJECT_REPORT");
topHtmlFile = shared.userVars.get("*TOP_HTMLFRAG_FILE");
bottomHtmlFile = shared.userVars.get("*BOTTOM_HTMLFRAG_FILE");
dsvRecordsPerCommit = 0;
if (shared.userVars.get("*DSV_RECORDS_PER_COMMIT") != null) try {
dsvRecordsPerCommit = Integer.parseInt(
shared.userVars.get("*DSV_RECORDS_PER_COMMIT"));
} catch (NumberFormatException nfe) {
errprintln(SqltoolRB.reject_rpc.getString(
shared.userVars.get("*DSV_RECORDS_PER_COMMIT")));
shared.userVars.remove("*DSV_RECORDS_PER_COMMIT");
}
nullRepToken = convertEscapes(shared.userVars.get("*NULL_REP_TOKEN"));
if (nullRepToken == null) nullRepToken = DEFAULT_NULL_REP;
nullRepHtml = shared.userVars.get("*NULL_REP_HTML");
if (nullRepHtml == null) nullRepHtml = DEFAULT_NULL_HTML;
timestampFormat = null;
String formatString = shared.userVars.get("*TIMESTAMP_FORMAT");
if (formatString != null) try {
timestampFormat = new SimpleDateFormat(formatString);
} catch (IllegalArgumentException iae) {
errprintln(SqltoolRB.bad_time_format.getString(
formatString, iae.getMessage()));
shared.userVars.remove("*TIMESTAMP_FORMAT");
}
}
/**
* 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;
// Since we enforce non-null mapping values, userVars.get(key) of null
// always means that that the map does not contain the specified key.
Map userVars = new HashMap();
Map macros = new HashMap();
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 final String revString = "$Revision: 6266 $";
private static final int revStringLength = revString.length();
private static final String revnum =
(revStringLength - " $".length() > "$Revision: ".length())
? revString.substring("$Revision: ".length(),
revStringLength - " $".length())
: "";
private static String DSV_OPTIONS_TEXT;
private static String D_OPTIONS_TEXT;
/**
* Convenience wrapper for the SqlFile(File, String) constructor
* @param inputFile input file
* @throws IOException on read errors
* @see #SqlFile(File, String)
*/
public SqlFile(final File inputFile) throws IOException {
this(new URL("file", null, inputFile.getPath()), null);
}
public SqlFile(final URL inputUrl) throws IOException {
this(inputUrl, null);
}
/**
* Convenience wrapper for the SqlFile(File, String, boolean) constructor
*
* @param inputFile the input File
* @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 on read errors
* @see #SqlFile(File, String, boolean)
*/
public SqlFile(final File inputFile, final String encoding)
throws IOException {
this(new URL("file", null, inputFile.getPath()), encoding, false);
}
public SqlFile(final URL inputUrl, final String encoding)
throws IOException {
this(inputUrl, encoding, false);
}
/**
* Constructor for non-interactive usage with a SQL file, using the
* specified encoding and sending normal output to stdout.
*
* @param inputFile the input File
* @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 on read errors
* @see #SqlFile(Reader, String, PrintStream, String, boolean, File)
*/
public SqlFile(final File inputFile, final String encoding,
final boolean interactive) throws IOException {
this(new URL("file", null, inputFile.getPath()), encoding, interactive);
}
public SqlFile(final URL inputUrl, final String encoding,
final boolean interactive) throws IOException {
this(new InputStreamReader(inputUrl.openStream(),
(encoding == null) ? DEFAULT_FILE_ENCODING : encoding),
inputUrl.toString(), System.out, encoding, interactive,
inputUrl);
}
/**
* 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 on read errors
* @see #SqlFile(Reader, String, PrintStream, String, boolean, File)
*/
public SqlFile(final String encoding, final boolean interactive)
throws IOException {
this((encoding == null)
? new InputStreamReader(System.in)
: new InputStreamReader(System.in, encoding),
"", System.out, encoding, interactive, (URL) null);
}
/**
* Legacy constructor. See following constructor for documentation.
* */
public SqlFile(final Reader reader, final String inputStreamLabel,
final PrintStream psStd, final String encoding,
final boolean interactive, final File atBaseFile)
throws IOException {
this(reader, inputStreamLabel, psStd, encoding, interactive,
atBaseFile == null ? ((URL) null) :
new URL("file", null, atBaseFile.getPath()));
}
/**
* Instantiate a SqlFile instance for SQL input from 'reader'.
*
* After any needed customization, the SQL can be executed by the
* execute method.
*
* Most Special Commands and many Buffer commands are only for
* interactive use.
*
* This program never writes to an error stream (stderr or alternative).
* All meta messages and error messages are written using the logging
* facility.
*
*
* @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 inputStreamLabel the lable
* @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 encoding the encoding
* @param interactive If true, prompts are printed, the interactive
* Special commands are enabled, and
* continueOnError defaults to true.
* @param atBase the base dir
* @throws IOException on read errors
* @see #execute()
*/
public SqlFile(final Reader reader, final String inputStreamLabel,
final PrintStream psStd, final String encoding,
final boolean interactive, final URL atBase) throws IOException {
this(reader, inputStreamLabel, atBase);
try {
shared = new SharedFields(psStd);
try {
String hostName = InetAddress.getLocalHost().getHostName();
shared.userVars.put("*HOSTNAME", hostName);
shared.userVars.put(
"*HOST", hostName.replaceFirst("[.].*$", ""));
} catch (UnknownHostException uhe) {
// Purposefully empty.
// We made a best effort but can't populate the *HOST variables.
}
shared.userVars.put(
"*START_TIME", (new java.util.Date()).toString());
shared.userVars.put("*REVISION", revnum);
shared.userVars.put("?", "");
shared.userVars.put("#", "0");
setEncoding(encoding);
this.interactive = interactive;
continueOnError = this.interactive;
if (interactive) {
history = new TokenList();
maxHistoryLength = DEFAULT_HISTORY_SIZE;
}
// 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, URL)
*/
private SqlFile(final SqlFile parentSqlFile, final URL inputUrl)
throws IOException {
this(parentSqlFile,
new InputStreamReader(inputUrl.openStream(),
(parentSqlFile.shared.encoding == null)
? DEFAULT_FILE_ENCODING : parentSqlFile.shared.encoding),
inputUrl.toString(), inputUrl);
}
/**
* Constructor for recursion
*/
private SqlFile(final SqlFile parentSqlFile, final Reader reader,
final String inputStreamLabel, final URL atBase)
throws MalformedURLException {
this(reader, inputStreamLabel, atBase);
try {
recursed = Recursion.FILE;
shared = parentSqlFile.shared;
// shared.userVars.put("?", ""); Don't destroy this useful value!
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.
} catch (RuntimeException re) {
closeReader();
throw re;
}
}
/**
* Base Constructor which every other Constructor starts with
*/
private SqlFile(final Reader reader, final String inputStreamLabel,
final URL atBase) throws MalformedURLException {
logger.privlog(Level.FINER, "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;
this.atBase = (atBase == null) ? new URL("file", null, ".") :
new URL(atBase.getProtocol(), atBase.getHost(), atBase.getFile().replace('\\', '/'));
}
public void setConnection(final 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(final boolean continueOnError) {
this.continueOnError = continueOnError;
}
public void setMaxHistoryLength(final 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(final Map newMacros) {
shared.macros.putAll(newMacros);
}
public void addUserVars(final Map newUserVars) {
for (String val : newUserVars.values()) {
if (val == null)
throw new IllegalArgumentException(
"Null mapping values not allowed");
}
shared.userVars.putAll(newUserVars);
List strangeVars = new ArrayList();
for (String name : newUserVars.keySet())
if (!name.equals("?") && !name.equals("#")
&& !varPattern.matcher(name).matches())
strangeVars.add(name);
if (strangeVars.size() > 0)
errprintln(SqltoolRB.varname_warning.getString(
strangeVars.toString()));
sqlExpandMode = null;
}
/**
* Get a reference to the user variable map.
*
* Since you are getting a reference to the private map used inside this
* class, update this map with great caution and attention to lifecycle
* handling of the variable map.
*
* @return user variable map
*/
public Map getUserVars() {
return shared.userVars;
}
public Map 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(final String newEncoding)
throws UnsupportedEncodingException {
if (newEncoding == null || newEncoding.length() < 1) {
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 Recursion 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, prevToken;
private boolean preempt;
private String lastSqlStatement;
private boolean autoClose = true;
private boolean csvStyleQuoting;
/**
* Specify whether the supplied or generated input Reader should
* automatically be closed by the execute() method.
*
* 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.
*
* The caller is always responsible for closing the output object (if any)
* used by SqlFile.
*
* @param autoClose boolean
*/
public void setAutoClose(final 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 SQLException 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");
updateUserSettings();
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(SqltoolRB.suggest_j.getString());
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(SqltoolRB.pipeclose_failure.getString(ioe));
}
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
}
}
/**
* If command is not an inline-nest command, returns null;
*
* @return Matcher which has already successfully .matched() or null
*/
private Matcher inlineNestMatcher(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 (!inlineNestPLCommands.containsKey(commandWord)) return null;
Pattern pattern = inlineNestPLCommands.get(commandWord);
Matcher m = pattern.matcher(token.val);
return m.matches() ? m : null;
}
/**
* 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(final 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());
}
/**
* Sets sqlExpandMode to true if any end-user userVar is set.
*/
private void setSqlExpandMode() {
for (String key : shared.userVars.keySet()) {
if (key.charAt(0) != '*' && !key.equals("?") && !key.equals("#")) {
sqlExpandMode = Boolean.TRUE;
return;
}
}
sqlExpandMode = Boolean.FALSE;
}
synchronized protected void scanpass(final TokenSource ts)
throws SqlToolError, SQLException {
boolean rollbackUncoms = true;
String nestingCommand;
Matcher inlineNestMatcher;
Token token = null;
sqlExpandMode = null;
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;
inlineNestMatcher = inlineNestMatcher(token);
if (inlineNestMatcher != null) {
processInlineBlock(token,
inlineNestMatcher.group(1),
inlineNestMatcher.group(2));
processBlock(token);
continue;
}
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();
ResultSet rs = null;
Statement statement = processSQL();
if (statement != null) {
try {
rs = statement.getResultSet();
displaySqlResults(
statement, rs, null, null, true);
} finally {
assert statement != null;
try {
statement.close();
} catch (SQLException nse) {
// Purposefully doing nothing
} finally {
statement = null;
}
}
}
}
continue;
case Token.MACRO_TYPE:
processMacro(token);
continue;
case Token.PL_TYPE:
// Storing prevToken as an attempted hack
prevToken = buffer;
setBuf(token);
historize();
processPL();
continue;
case Token.SPECIAL_TYPE:
// Storing prevToken as an attempted hack
prevToken = buffer;
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();
ResultSet rs = null;
Statement statement = processSQL();
if (statement != null) {
try {
rs = statement.getResultSet();
displaySqlResults(
statement, rs, null, null, true);
} finally {
assert statement != null;
try {
statement.close();
} catch (SQLException nse) {
// Purposefully doing nothing
} finally {
statement = null;
}
}
}
continue;
default:
assert false :
"Internal assertion failed. Unexpected token type: "
+ token.getTypeString();
}
} catch (BadSpecial bs) {
// BadSpecials ALWAYS have non-null getMessage().
assert token != null;
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));
if (!continueOnError) throw new SqlToolError(bs);
} catch (SQLException se) {
//se.printStackTrace();
assert token != null;
errprintln("SQL " + SqltoolRB.errorat.getString(
inputStreamLabel, 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 != null) {
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 != null || !continueOnError) throw be;
} catch (ContinueException ce) {
String msg = ce.getMessage();
if (recursed != null) {
rollbackUncoms = false;
} else {
errprintln(SqltoolRB.continue_unsatisfied.getString(msg));
}
if (recursed != null || !continueOnError) throw ce;
} catch (QuitNow qn) {
throw qn;
} catch (SqlToolError ste) {
assert token != null;
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.
*/
(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));
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 != null)
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));
fetchingVar = null;
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(final String s) {
super(s);
assert s != null:
"Must construct BadSpecials with non-null message";
}
BadSpecial(final String s, final Throwable t) {
super(s, t);
assert s != null:
"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 static class QuitNow extends SqlToolError {
static final long serialVersionUID = 1811094258670900488L;
public QuitNow(final 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 static class BreakException extends SqlToolError {
static final long serialVersionUID = 351150072817675994L;
public BreakException() {
super();
}
public BreakException(final 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 static class ContinueException extends SqlToolError {
static final long serialVersionUID = 5064604160827106014L;
public ContinueException() {
super();
}
public ContinueException(final String s) {
super(s);
}
}
/**
* Utility nested Exception class for internal use only.
*/
private static class BadSubst extends Exception {
static final long serialVersionUID = 7325933736897253269L;
BadSubst(final String s) {
super(s);
}
}
/**
* Utility nested Exception class for internal use only.
*/
private static class RowError extends AppendableException {
static final long serialVersionUID = 754346434606022750L;
RowError(final String s) {
super(s);
}
/* Unused so far
RowError(Throwable t) {
this(null, t);
}
*/
RowError(final String s, final 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 token 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(final 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' :
if (other != null)
throw new BadSpecial(
SqltoolRB.special_extrachars.getString("l", other));
if (buffer == null) {
stdprintln(nobufferYetString);
} else {
stdprintln(SqltoolRB.editbuffer_contents.getString(
buffer.reconstitute()));
}
return;
case 'h' :
if (other != null)
throw new BadSpecial(
SqltoolRB.special_extrachars.getString("h", other));
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.trim().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 ';' :
if (other != null)
throw new BadSpecial(
SqltoolRB.special_extrachars.getString(";", 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:
assert false: "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());
File targetFile =
dereferenceAtToFile(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.getAbsolutePath()), 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 ':'.
assert m.groupCount() > 2 && m.groupCount() < 5 :
"Internal assertion failed. "
+ "Matched substitution pattern, but captured "
+ m.groupCount() + " groups";
String optionGroup = (
(m.groupCount() > 3 && m.group(4) != null)
? 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 void enforce1charSpecial(final String tokenString,
final char command) throws BadSpecial {
if (tokenString.length() != 1)
throw new BadSpecial(SqltoolRB.special_extrachars.getString(
Character.toString(command), tokenString.substring(1)));
}
/**
* 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(final 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(dereference(string, false));
if (!m.matches())
throw new BadSpecial(SqltoolRB.special_malformat.getString());
// I think it's impossible to get here, since the pattern is
// so liberal.
assert m.groupCount() > 0 && m.groupCount() < 3:
"Internal assertion failed. Pattern matched, yet captured "
+ m.groupCount() + " groups";
String arg1 = m.group(1);
// If other count > 1 and group(2) not null, then it is non-empty.
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.trim());
throw new QuitNow();
case 'H' :
case 'h' :
enforce1charSpecial(arg1, 'h');
htmlMode = (other == null) ? (!htmlMode)
: Boolean.parseBoolean(other.trim());
shared.psStd.println(SqltoolRB.html_mode.getString(
Boolean.toString(htmlMode)));
return;
case 'm' :
if (arg1.equals("m?") || arg1.equals("mq?")
|| (other != null && other.trim().equals("?")
&& (arg1.equals("m") || arg1.equals("mq")))) {
stdprintln(DSV_OPTIONS_TEXT + LS + DSV_M_SYNTAX_MSG);
return;
}
shared.userVars.remove("?");
requireConnection();
if ((!arg1.equals("mq") && arg1.length() != 1)
|| other == null)
throw new BadSpecial(DSV_M_SYNTAX_MSG);
other = other.trim();
String skipPrefix = dsvSkipPrefix;
if (other.charAt(other.length() - 1) == '*') {
other = other.substring(0, other.length()-1).trim();
if (other.length() < 1)
throw new BadSpecial(DSV_M_SYNTAX_MSG);
skipPrefix = null;
}
csvStyleQuoting = arg1.equals("mq");
try {
importDsv(dereferenceAtToUrl(other), skipPrefix);
} finally {
csvStyleQuoting = false;
}
shared.userVars.put("?", "");
return;
case 'x' :
if (arg1.equals("x?") || arg1.equals("xq?")
|| (other != null && other.trim().equals("?")
&& (arg1.equals("x") || arg1.equals("xq")))) {
stdprintln(DSV_OPTIONS_TEXT + LS + DSV_X_SYNTAX_MSG);
return;
}
shared.userVars.remove("?");
requireConnection();
try {
if ((!arg1.equals("xq") && arg1.length() != 1)
|| other == null)
throw new BadSpecial(DSV_X_SYNTAX_MSG);
String tableName = null;
StringBuilder query = new StringBuilder();
// Due to regex, other does contain at least one \S.
if (other.trim().charAt(0) == ':') {
// Case 1: Query from Edit-buffer
if (prevToken == null)
throw new BadSpecial(nobufferYetString);
query.append(prevToken.val)
.append(other.substring(other.indexOf(':')+1));
} else if (wordAndDotPattern.matcher(
other.trim()).matches()) {
// Case 2: Table name specified
tableName = other.trim();
query.append("SELECT * FROM ").append(tableName);
} else {
// Case 3: Explicit in-line query
query.append(other.trim());
}
if (dsvTargetFile == null && tableName == null)
throw new BadSpecial(
SqltoolRB.dsv_targetfile_demand.getString());
ResultSet rs = null;
Statement st = null;
File dsvFile = null;
csvStyleQuoting = arg1.equals("xq");
try {
dsvFile = dsvTargetFile == null
? new File(tableName
+ (csvStyleQuoting ? ".csv" : ".dsv"))
: dereferenceAtToFile(dsvTargetFile);
pwDsv = new PrintWriter(new OutputStreamWriter(
new FileOutputStream(dsvFile),
(shared.encoding == null)
? DEFAULT_FILE_ENCODING : shared.encoding));
st = shared.jdbcConn.createStatement();
rs = st.executeQuery(query.toString());
List colList = new ArrayList();
int[] incCols = null;
if (dsvSkipCols != null) {
Set skipCols = new HashSet();
for (String s : dsvSkipCols.split(
"\\Q" + 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();
}
displaySqlResults(st, rs, incCols, null, true);
} finally {
csvStyleQuoting = false;
if (rs != null) try {
rs.close();
} catch (SQLException se) {
// Purposefully empty
} finally {
rs = null;
}
if (st != null) try {
st.close();
} catch (SQLException se) {
// Purposefully empty
} finally {
st = null;
}
}
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' :
if (arg1.equals("d?") ||
(arg1.equals("d") && other != null
&& other.trim().equals("?"))) {
stdprintln(D_OPTIONS_TEXT);
return;
}
requireConnection();
if (arg1.length() == 2) {
listTables(arg1.charAt(1),
(other == null) ? null : other.trim());
return;
}
if (arg1.length() == 1 && other != null) try {
other = other.trim();
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' :
boolean addFooter = arg1.equals("oc");
if (addFooter) arg1 = "o";
enforce1charSpecial(arg1, 'o');
if (other == null) {
if (pwQuery == null)
throw new BadSpecial(
SqltoolRB.outputfile_nonetoclose.getString());
if (addFooter)
writeFooter(pwQuery, "(the HTML report file)");
closeQueryOutputStream();
return;
}
other = other.trim();
if (pwQuery != null) {
shared.psStd.println(
SqltoolRB.outputfile_reopening.getString());
closeQueryOutputStream();
}
File file = dereferenceAtToFile(other);
boolean preExists = file.exists();
try {
pwQuery = new PrintWriter(new OutputStreamWriter(
new FileOutputStream(file, true),
(shared.encoding == null)
? DEFAULT_FILE_ENCODING : shared.encoding));
} catch (Exception e) {
throw new BadSpecial(SqltoolRB.file_writefail.getString(
file.getAbsolutePath()), e);
}
/* Opening in append mode, so it's possible that we will
* be adding superfluous and tags.
* I think that browsers can handle that */
if (htmlMode && !preExists)
writeHeader(pwQuery, file.getPath());
pwQuery.flush();
return;
case 'i' :
enforce1charSpecial(arg1, 'i');
if (other == null)
throw new BadSpecial(
SqltoolRB.sqlfile_name_demand.getString());
other = other.trim();
sqlExpandMode = null;
try {
new SqlFile(this, dereferenceAtToUrl(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);
}
updateUserSettings();
return;
case 'p' :
if (arg1.equals("pr")) {
if (other == null) {
if (shared.psStd != null) shared.psStd.println();
if (pwQuery != null) {
pwQuery.println();
pwQuery.flush();
}
} else {
shared.psStd.println(other);
if (pwQuery != null) {
pwQuery.println(other);
pwQuery.flush();
}
}
return;
}
if (arg1.equals("pn")) {
// No-op if other == null
if (other != null) stdprint(other, true);
return;
}
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;
assert level != null:
"Internal assertion failed. "
+ " Unexpected Level string: " + levelString;
logger.enduserlog(level, logMatcher.group(2));
}
return;
case 'a' :
requireConnection();
enforce1charSpecial(arg1, 'a');
if (other != null) {
other = other.trim();
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.trim().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;
default:
assert false;
}
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(
SqltoolRB.connect_failure.getString(), e);
}
displayConnBanner();
} catch (Throwable t) {
t.printStackTrace();
return;
}
return;
case 'v' :
requireConnection();
enforce1charSpecial(arg1, 'v');
if (other != null) {
other = other.trim();
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());
other = other.trim();
try {
if (arg1.charAt(1) == 'd') {
dump(dereferenceAtToFile(other));
} else {
binBuffer =
SqlFile.loadBinary(dereferenceAtToUrl(other));
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.trim());
stdprintln(SqltoolRB.exectime_reporting.getString(
Boolean.toString(reportTimes)));
return;
case 'c' :
enforce1charSpecial(arg1, '=');
if (other != null)
// But remember that we have to abort on some I/O errors.
continueOnError = Boolean.parseBoolean(other.trim());
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.trim());
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 && !ignoreBangStatus)
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(final String inString, final 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 on error
*/
private String dereference(final String inString,
final 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) break; // No more unexpanded variable uses
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) break; // No more unexpanded variable uses
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) { // Key not in map, since never null vals.
if (varName.equals("*TIMESTAMP")) {
if (timestampFormat == null)
throw new SqlToolError(
SqltoolRB.no_timestamp_format.getString());
varValue = timestampFormat.format(new java.util.Date());
} else if (varName.equals("*SCRIPT")) {
varValue = inputStreamLabel;
} else if (varName.equals("*SCRIPT_FILE")) {
varValue = inputStreamLabel.replaceFirst("^.*[/\\\\]", "");
} else if (varName.equals("*SCRIPT_BASE")) {
varValue = inputStreamLabel.replaceFirst("^.*[/\\\\]", "")
.replaceFirst("[.][^.]*$", "");
} else {
if (!permitUnset)
throw new SqlToolError(
SqltoolRB.plvar_undefined.getString(varName));
varValue = "";
}
}
expandBuffer.replace(b, e + 1, varValue);
}
return expandBuffer.toString();
}
private Boolean sqlExpandMode; // Null indicates dirty.
// PL variable name currently awaiting query output.
private String fetchingVar;
private boolean silentFetch;
private boolean fetchBinary;
/**
* Process a block PL command like "if" or "foreach".
*
* @throws SQLException only if thrown by *forrows processing.
*/
private void processBlock(final Token token)
throws BadSpecial, SqlToolError, SQLException {
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) {
stdprintln(SqltoolRB.deprecated_noop.getString("*"));
return;
}
String[] tokens = m.group(1).split("\\s+", -1);
if (tokens[0].equals("for")) {
Matcher forM = forPattern.matcher(
dereference(token.val, false));
if (!forM.matches())
throw new BadSpecial(
SqltoolRB.pl_malformat_specific.getString("for"));
assert forM.groupCount() == 2 || forM.groupCount() == 3:
"Internal assertion failed. "
+ "forh pattern matched, but captured "
+ forM.groupCount() + " groups";
String iterableAssignmentStr = forM.group(forM.groupCount());
String logicalExprStr = forM.group(forM.groupCount() - 1);
String initAssignmentStr = (forM.groupCount() < 3
|| forM.group(1) == null
|| forM.group(1).trim().length() < 1)
? null : forM.group(1);
if (initAssignmentStr != null) try {
Matcher mathMatcher =
mathAsgnPattern.matcher(initAssignmentStr);
if (mathMatcher.matches()) {
shared.userVars.put(mathMatcher.group(1), Long.toString(
Calculator.reassignValue(mathMatcher.group(1),
shared.userVars, mathMatcher.group(2),
(mathMatcher.groupCount() < 3)
? null : mathMatcher.group(3))));
} else {
mathMatcher = mathPattern.matcher(initAssignmentStr);
if (mathMatcher.matches())
shared.userVars.put(mathMatcher.group(1), Long.toString(
new Calculator(((mathMatcher.groupCount() > 1
&& mathMatcher.group(2) != null)
? mathMatcher.group(2)
: ""), shared.userVars).reduce(0, false)));
}
sqlExpandMode = null;
} catch (RuntimeException re) {
throw new BadSpecial(SqltoolRB.math_expr_fail.getString(re));
}
String[] values =
logicalExprStr.substring(1, logicalExprStr.length() - 1)
.replaceAll("!([a-zA-Z0-9*])", "! $1").
replaceAll("([a-zA-Z0-9*])!", "$1 !").split("\\s+", -1);
try {
while (eval(values)) {
Recursion origRecursed = recursed;
recursed = Recursion.FOR;
try {
scanpass(token.nestedBlock.dup());
} catch (ContinueException ce) {
String ceMessage = ce.getMessage();
if (ceMessage != null && !ceMessage.equals("for"))
throw ce;
} finally {
recursed = origRecursed;
}
try {
Matcher mathMatcher =
mathAsgnPattern.matcher(iterableAssignmentStr);
if (mathMatcher.matches()) {
shared.userVars.put(
mathMatcher.group(1), Long.toString(
Calculator.reassignValue(
mathMatcher.group(1),
shared.userVars, mathMatcher.group(2),
(mathMatcher.groupCount() < 3)
? null : mathMatcher.group(3))));
} else {
mathMatcher =
mathPattern.matcher(iterableAssignmentStr);
if (mathMatcher.matches())
shared.userVars.put(
mathMatcher.group(1), Long.toString(
new Calculator(
((mathMatcher.groupCount() > 1
&& mathMatcher.group(2) != null)
? mathMatcher.group(2)
: ""),
shared.userVars).reduce(0, false)));
}
} catch (RuntimeException re) {
throw new BadSpecial(
SqltoolRB.math_expr_fail.getString(re));
}
// No updateUserSettings since can't modify *System vars
sqlExpandMode = null;
}
} catch (BreakException be) {
String beMessage = be.getMessage();
// Handle "for" and plain breaks (by doing nothing)
if (beMessage != null && !beMessage.equals("for")) 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);
} finally {
// If we haven't instantiated a new SqlTool, then the following
// are unnecessary. TODO: Test this and remove if unnecessary.
updateUserSettings();
sqlExpandMode = null;
}
return;
}
if (tokens[0].equals("forrows")) {
Matcher forrowsM = forrowsPattern.matcher(
dereference(token.val, false));
if (!forrowsM.matches())
throw new BadSpecial(
SqltoolRB.pl_malformat_specific.getString("forrows"));
String[] vars = (forrowsM.groupCount() > 0
&& forrowsM.group(1) != null
&& forrowsM.group(1).length() > 0)
? forrowsM.group(1).trim().split("\\s+") : null;
String[] origVals = (vars == null) ? null : new String[vars.length];
if (origVals != null) for (int i = 0; i < vars.length; i++)
origVals[i] = shared.userVars.get(vars[i]);
TokenList dupNesteds = token.nestedBlock.dup();
if (dupNesteds.size() < 1)
// TODO: Define message. Used to test for <2. Don't know if
// it's possible for it to be < 1.
throw new BadSpecial("Empty forrows loop");
Token queryToken = dupNesteds.remove(0);
if (queryToken.type != Token.SQL_TYPE)
// TODO: Define message
throw new BadSpecial("*forrows command not followed "
+ "immediately by an SQL statement");
setBuf(queryToken);
List rowData = new ArrayList();
ResultSet rs = null;
int colCount = 0;
Statement statement = processSQL();
if (statement == null)
// TODO: Define message
throw new BadSpecial("Failed to prepare SQL for loop");
shared.userVars.put("#", "0");
shared.userVars.remove("*ROWS");
List dsvRows = new ArrayList();
try {
rs = statement.getResultSet();
shared.userVars.put("#", Integer.toString(rowData.size()));
ResultSetMetaData rsmd = rs.getMetaData();
colCount = rsmd.getColumnCount();
if (vars != null && vars.length > colCount)
// TODO: Define message
throw new BadSpecial("*forrows command specifies "
+ vars.length
+ " variables, but query pulled only "
+ colCount + " columns");
if (colCount < 1) return;
String[] rowCells;
while (rs.next()) {
rowCells = new String[colCount];
rowData.add(rowCells);
for (int i = 1; i <= colCount; i++)
rowCells[i-1] = rs.getString(i);
}
} finally {
try {
if (rs != null) rs.close();
} catch (SQLException nse) {
// Purposefully doing nothing
} finally {
rs = null;
}
try {
statement.close();
} catch (SQLException nse) {
// Purposefully doing nothing
} finally {
statement = null;
}
}
lastSqlStatement = null;
// Done with SQL
shared.userVars.put("#", Integer.toString(rowData.size()));
if (rowData.size() > 0) {
String firstVal = rowData.get(0)[0];
String lastVal = rowData.get(rowData.size()-1)[colCount - 1];
shared.userVars.put("?",
(lastVal == null) ? nullRepToken : lastVal);
if (fetchingVar != null) {
if (firstVal == null)
shared.userVars.remove(fetchingVar);
else
shared.userVars.put(fetchingVar, firstVal);
updateUserSettings();
sqlExpandMode = null;
fetchingVar = null;
}
} else {
shared.userVars.put("?", "");
}
StringBuilder rowBuilder = new StringBuilder();
String rowVal;
try {
for (String[] cells : rowData) {
if (cells.length == 1) {
rowVal = (cells[0] == null) ? nullRepToken : cells[0];
} else {
rowBuilder.setLength(0);
for (String s : cells) {
if (rowBuilder.length() > 0)
rowBuilder.append(dsvColDelim);
rowBuilder.append((s == null) ? nullRepToken : s);
}
rowVal = rowBuilder.toString();
}
shared.userVars.put("*ROW", rowVal);
dsvRows.add(shared.userVars.get("*ROW"));
if (vars != null) for (int i = 0; i < vars.length; i++)
if (cells[i] == null)
shared.userVars.remove(vars[i]);
else
shared.userVars.put(vars[i], cells[i]);
updateUserSettings();
Recursion origRecursed = recursed;
recursed = Recursion.FORROWS;
try {
scanpass(dupNesteds.dup());
} catch (ContinueException ce) {
String ceMessage = ce.getMessage();
if (ceMessage != null
&& !ceMessage.equals("forrows")) throw ce;
} finally {
recursed = origRecursed;
}
}
} catch (BreakException be) {
String beMessage = be.getMessage();
// Handle "forrows" and plain breaks (by doing nothing)
if (beMessage != null && !beMessage.equals("forrows")) 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);
} finally {
shared.userVars.remove("*ROW");
if (origVals != null) for (int i = 1; i < origVals.length; i++)
if (origVals[i] == null)
shared.userVars.remove(vars[i]);
else
shared.userVars.put(vars[i], origVals[i]);
// Simply want to do String.join(delim, strings), but that's
// not in Java until v8.
StringBuilder sb = new StringBuilder();
for (String dsvRow : dsvRows) {
if (sb.length() > 0) sb.append(dsvRowDelim);
sb.append(dsvRow);
}
shared.userVars.put("*ROWS", sb.toString());
updateUserSettings();
sqlExpandMode = null;
}
return;
}
if (tokens[0].equals("foreach")) {
Matcher foreachM = foreachPattern.matcher(
dereference(token.val, false));
if (!foreachM.matches())
throw new BadSpecial(
SqltoolRB.pl_malformat_specific.getString("foreach"));
if (foreachM.groupCount() != 2)
assert foreachM.groupCount() == 2:
"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());
if (!varPattern.matcher(varName).matches())
errprintln(SqltoolRB.varname_warning.getString(varName));
String[] values = foreachM.group(2).split("\\s+", -1);
String origval = shared.userVars.get(varName);
try {
for (String val : values) {
// val may never be null
shared.userVars.put(varName, val);
updateUserSettings();
Recursion origRecursed = recursed;
recursed = Recursion.FOREACH;
try {
scanpass(token.nestedBlock.dup());
} catch (ContinueException ce) {
String ceMessage = ce.getMessage();
if (ceMessage != null
&& !ceMessage.equals("foreach")) throw ce;
} finally {
recursed = origRecursed;
}
}
} 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);
} finally {
if (origval == null) {
shared.userVars.remove(varName);
} else {
shared.userVars.put(varName, origval);
}
updateUserSettings();
sqlExpandMode = null;
}
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());
assert ifwhileM.groupCount() == 1:
"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 {
// Provisionally 'else'. Will be nulled if it is not:
Token elseToken = (token.nestedBlock.size() < 1)
? null
: token.nestedBlock.get(
token.nestedBlock.size() - 1);
if (elseToken != null && (elseToken.type != Token.PL_TYPE ||
!elseToken.val.equals("else"))) elseToken = null;
//if (elseToken != null)
//token.nestedBlock.remove(token.nestedBlock.size() - 1);
Token recurseToken = eval(values) ? token : elseToken;
if (recurseToken != null) {
Recursion origRecursed = recursed;
recursed = Recursion.IF;
try {
scanpass(recurseToken.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.pl_malformat_specific.getString("if"));
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)) {
Recursion origRecursed = recursed;
recursed = Recursion.WHILE;
try {
scanpass(token.nestedBlock.dup());
} catch (ContinueException ce) {
String ceMessage = ce.getMessage();
if (ceMessage != null && !ceMessage.equals("while"))
throw ce;
} finally {
recursed = origRecursed;
}
}
} 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.pl_malformat_specific.getString("while"));
throw bs;
} catch (RuntimeException re) {
throw re; // Unrecoverable
} catch (Exception e) {
throw new BadSpecial(
SqltoolRB.pl_block_fail.getString(), e);
}
} else {
assert false: 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.
*
* @throws BadSpecial special-command-specific errors.
* @throws SqlToolError all other errors, plus BreakException and
* ContinueException.
*/
private void processPL() throws BadSpecial, SqlToolError {
String string = buffer.val;
String dereffed = dereference(string, false);
Matcher mathMatcher = mathAsgnPattern.matcher(dereffed);
if (mathMatcher.matches()) try {
shared.userVars.put(mathMatcher.group(1), Long.toString(
Calculator.reassignValue(mathMatcher.group(1),
shared.userVars, mathMatcher.group(2),
(mathMatcher.groupCount() < 3)
? null : mathMatcher.group(3))));
// No updateUserSettings since can't modify *System vars
sqlExpandMode = null;
return;
} catch (RuntimeException re) {
throw new BadSpecial(SqltoolRB.math_expr_fail.getString(re));
}
mathMatcher = mathPattern.matcher(dereffed);
if (mathMatcher.matches()) try {
shared.userVars.put(mathMatcher.group(1), Long.toString(
new Calculator(((mathMatcher.groupCount() > 1
&& mathMatcher.group(2) != null) ? mathMatcher.group(2)
: ""), shared.userVars).reduce(0, false)));
// No updateUserSettings since can't modify *System vars
sqlExpandMode = null;
return;
} catch (RuntimeException re) {
throw new BadSpecial(SqltoolRB.math_expr_fail.getString(re));
}
Matcher m = plPattern.matcher(dereffed);
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) {
stdprintln(SqltoolRB.deprecated_noop.getString("*"));
return;
}
String[] tokens = m.group(1).split("\\s+", -1);
if (tokens[0].charAt(0) == '?') {
String remainder = tokens[0].substring(1);
String msg = null;
if (remainder.startsWith("assign") ||
(tokens.length > 1 && tokens[1].startsWith("assign")))
msg = SqltoolRB.pl_assign.getString();
else if (remainder.equals("control") ||
(tokens.length > 1 && tokens[1].equals("control")))
msg = SqltoolRB.pl_control.getString();
else
msg = SqltoolRB.pl_help.getString();
stdprintln(msg);
return;
}
if (tokens[0].equals("else")) {
if (recursed != Recursion.IF)
throw new BadSpecial(SqltoolRB.else_without_if.getString());
return;
}
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("forrows")
|| tokens[1].equals("for")
|| tokens[1].equals("while")))
throw new ContinueException(tokens[1]);
throw new BadSpecial(SqltoolRB.continue_syntax.getString());
}
throw new ContinueException();
}
if (tokens[0].equals("return")) {
if (tokens.length > 1)
throw new BadSpecial(SqltoolRB.break_syntax.getString());
throw new BreakException("file");
}
if (tokens[0].equals("break")) {
if (tokens.length > 1) {
if (tokens.length == 2 &&
(tokens[1].equals("foreach")
|| tokens[1].equals("forrows")
|| tokens[1].equals("while")
|| tokens[1].equals("for")
|| 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());
if (tokens[0].equals("dump")) {
File dlFile = dereferenceAtToFile(tokens[2]);
try {
dump(varName, dlFile);
} catch (IOException ioe) {
throw new BadSpecial(SqltoolRB.dumpload_fail.getString(
varName, dlFile.toString()), ioe);
}
} else {
URL url = dereferenceAtToUrl(tokens[2]);
try {
load(varName, url, shared.encoding);
} catch (IOException ioe) {
throw new BadSpecial(SqltoolRB.dumpload_fail.getString(
varName, url.toString()), ioe);
}
}
return;
}
if (tokens[0].equals("prepare")) {
if (tokens.length != 2)
throw new BadSpecial(
SqltoolRB.pl_malformat_specific.getString("prepare"));
if (shared.userVars.get(tokens[1]) == null)
throw new BadSpecial(
SqltoolRB.plvar_undefined.getString(tokens[1]));
prepareVar = tokens[1];
doPrepare = true;
return;
}
if (tokens[0].equals("-")) {
// We do not consider it an error to remove a non-existent
// variable.
if (tokens.length != 2)
throw new BadSpecial(
SqltoolRB.pl_unset_nomoreargs.getString());
if (fetchingVar != null && fetchingVar.equals(tokens[1]))
fetchingVar = null;
if (tokens[1].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) {
// Impossible to get here. Satisfy compiler.
throw new BadSpecial(
SqltoolRB.encode_fail.getString(m.group(3)));
}
shared.userVars.remove(tokens[1]);
updateUserSettings();
sqlExpandMode = null;
return;
}
String derefed = dereference(string, false);
m = varsetPattern.matcher(derefed);
if (!m.matches())
throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0]));
assert m.groupCount() > 1 && m.groupCount() < 4:
"varset pattern matched but captured " + m.groupCount() + " groups";
String varName = m.group(1);
// Test for ambiguous form: * VARNAME_
// Ambibuous whether _ is part of varname or operator.
if (derefed.trim().equals(varName + '_'))
throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0]));
if (varName.indexOf(':') > -1)
throw new BadSpecial(SqltoolRB.plvar_nocolon.getString());
if (!varPattern.matcher(varName).matches())
errprintln(SqltoolRB.varname_warning.getString(varName));
switch (m.group(2).charAt(0)) {
case ':' :
if (prevToken == null) throw new BadSpecial(nobufferYetString);
StringBuilder sb = new StringBuilder();
switch (prevToken.type) {
case Token.PL_TYPE:
sb.append('*');
break;
case Token.SPECIAL_TYPE:
sb.append('\\');
break;
default:
// Intentionally empty
}
sb.append(prevToken.val);
if (m.groupCount() > 2 && m.group(3) != null)
sb.append(m.group(3));
shared.userVars.put(varName, sb.toString());
updateUserSettings();
sqlExpandMode = null;
return;
case '_' :
silentFetch = true;
// Purposefully fall through to next case
case '~' :
// TODO: Condsider limiting fetchingVars to User variables
// (as opposed to *SYSTEM variables).
// That would eliminate some updateUserSettings calls.
if (m.groupCount() > 2 && m.group(3) != null
&& m.group(3).trim().length() > 0) {
throw new BadSpecial(
SqltoolRB.plvar_tildedash_nomoreargs.getString(
m.group(3).trim()));
}
shared.userVars.remove(varName);
updateUserSettings();
sqlExpandMode = null;
fetchingVar = varName;
return;
case '=' :
if (fetchingVar != null && fetchingVar.equals(varName))
fetchingVar = null;
String varVal = (m.groupCount() > 2 && m.group(3) != null)
? m.group(3).replaceFirst("^\\s+", "") : null;
if (varVal != null && varVal.length() < 1) varVal = 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(varVal.trim());
return;
} catch (UnsupportedEncodingException use) {
throw new BadSpecial(
SqltoolRB.encode_fail.getString(varVal));
}
if (varVal == null) {
if (removeEmptyVars()) {
stdprintln(SqltoolRB.
remove_empty_vars_suggestset.getString());
shared.userVars.remove(varName);
} else {
shared.userVars.put(varName, "");
}
} else {
shared.userVars.put(varName, varVal);
}
updateUserSettings();
sqlExpandMode = null;
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(final String s) {
stdprint(s, false);
}
private void stdprintln(final String s) {
stdprintln(s, false);
}
/**
* Encapsulates normal output.
*
* Conditionally HTML-ifies output.
*/
private void stdprintln(final boolean queryOutput) {
if (shared.psStd != null) {
if (htmlMode) {
shared.psStd.println("
");
} else {
shared.psStd.println();
}
}
if (queryOutput && pwQuery != null) {
if (htmlMode) {
pwQuery.println("
");
} else {
pwQuery.println();
}
pwQuery.flush();
}
}
/**
* Encapsulates error output.
*
* Conditionally HTML-ifies error output.
*/
private void errprintln(final String s) {
if (pwQuery != null && htmlMode) {
pwQuery.println(""
+ SqlFile.escapeHtml(s) + "
");
pwQuery.flush();
}
if (shared.psStd != null && htmlMode) {
shared.psStd.println(""
+ SqlFile.escapeHtml(s) + "
");
} else {
logger.privlog(Level.SEVERE, s, null, 4, 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 ? ("" + SqlFile.escapeHtml(s) + "
") : s);
if (queryOutput && pwQuery != null) {
pwQuery.print(
htmlMode ? ("" + SqlFile.escapeHtml(s) + "
") : s);
pwQuery.flush();
}
}
/**
* Encapsulates normal output.
*
* Conditionally HTML-ifies output.
*/
private void stdprintln(String s, boolean queryOutput) {
shared.psStd.println(
htmlMode ? ("" + SqlFile.escapeHtml(s) + "
") : s);
if (queryOutput && pwQuery != null) {
pwQuery.println(
htmlMode ? ("" + SqlFile.escapeHtml(s) + "
") : 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_NULL_HTML = "Ø";
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
* - they are hidden from the casual user
*
- they are created by the installer at installation-time
*
- they are used automatically by the Oracle engine when the
* specific Oracle sub-product is used
*
- the accounts should not be messed with by database users
*
- the accounts should certainly not be used if the specific
* Oracle sub-product is going to be used.
*
*
* General advice: If you aren't going to use an Oracle sub-product,
* then don't install it!
* 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 displaySqlResults() 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(final char c, final 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) {
if (interactive)
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 'c' :
rs = md.getCatalogs();
if (rs == null)
throw new BadSpecial(
SqltoolRB.metadata_fetch_fail.getString());
displaySqlResults(null, rs, listMDSchemaCols, filter, false);
return;
case 'n' :
rs = md.getSchemas();
if (rs == null)
throw new BadSpecial(
SqltoolRB.metadata_fetch_fail.getString());
displaySqlResults(null, rs, listMDSchemaCols, filter, false);
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(
SqltoolRB.metadata_fetch_fail.getString());
displaySqlResults(null, rs, listMDIndexCols, null, false);
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());
displaySqlResults(statement, rs, listSet, filter, false);
if (additionalSchemas != null) {
for (String additionalSchema : additionalSchemas) {
/*
* Inefficient, but we have to do each successful query
* twice in order to prevent calling displaySqlResults
* for empty/non-existent schemas
*/
rs = md.getTables(null, additionalSchema, null,
types);
assert rs != null;
if (!rs.next()) continue;
displaySqlResults(
null,
md.getTables(null, additionalSchema, null, types),
listSet, filter, false);
}
}
} 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.
} finally {
rs = null;
}
}
if (statement != null) try {
statement.close();
} catch (SQLException se) {
// Purposefully doing nothing
} finally {
statement = null;
}
}
}
private boolean excludeSysSchemas;
/**
* Process the contents of Edit Buffer as an SQL Statement
*
* @return an open SQL Statement or null.
* @throws SQLException thrown by JDBC driver.
* @throws SqlToolError all other errors.
*/
private Statement processSQL() throws SQLException, SqlToolError {
shared.userVars.remove("?");
requireConnection();
assert buffer != null:
"Internal assertion failed. No buffer in processSQL().";
assert buffer.type == Token.SQL_TYPE:
"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.
if (sqlExpandMode == null) setSqlExpandMode();
lastSqlStatement = sqlExpandMode.booleanValue()
? dereference(buffer.val, true) : buffer.val;
// Above is the only case where we deference conditionally.
// For :, \, * commands we either always do or always don't.
// 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 {
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);
}
// We close SQL resources for following catches.
// If we return resources, we obviously do not close them.
// User already being notified about the problem so don't clutter
// with details about close failures.
} catch (SQLException se) {
if (statement != null) try {
statement.close();
} catch (SQLException sen) {
// Intentionally empty. See preceding comment
} finally {
statement = null;
}
throw se; // rethrow
} catch (SqlToolError ste) {
if (statement != null) try {
statement.close();
} catch (SQLException sen) {
// Intentionally empty. See preceding comment
} finally {
statement = null;
}
throw ste; // rethrow
} finally {
if (reportTimes) {
long elapsed = (new java.util.Date().getTime()) - startTime;
//condlPrintln("", 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) {
// The thing about this block is, we can't clean up resources
// because our connection has been severed. We just notify and
// return.
// 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 null;
}
return statement;
}
/**
* Display the given result set or update count for user and closes the
* supplied JDBC resources.
* 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).
*
* 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,
* and so we can close it.
* Can be null if you don't want either of those things.
* @param r The ResultSet to display and close.
* @param incCols Optional list of which columns to include (i.e., if
* given, then other columns will be skipped).
* @param filterString Optional filter. Rows are skipped which to not
* contain this substring in ANY COLUMN.
* (Should add another param to specify targeted columns).
* @param updateStatus status
* @throws SQLException thrown by JDBC driver.
* @throws SqlToolError all other errors.
*/
private void displaySqlResults(Statement statement,
ResultSet r, final int[] incCols, final String filterString,
final boolean updateStatus) throws SQLException, SqlToolError {
try {
if (pwDsv != null && csvStyleQuoting && (dsvColDelim.indexOf('"') > -1
|| dsvRowDelim.indexOf('"') > -1))
throw new SqlToolError(SqltoolRB.dsv_q_nodblquote.getString());
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));
}
if (excludeSysSchemas && interactive)
stdprintln(SqltoolRB.vendor_nosup_sysschemas.getString());
switch (updateCount) {
case -1 :
if (r == null) {
if (interactive)
stdprintln(SqltoolRB.noresult.getString(), true);
break;
}
shared.userVars.put("#", "0");
ResultSetMetaData m = r.getMetaData();
int cols = m.getColumnCount();
int incCount = (incCols == null) ? cols
: incCols
.length;
String val;
List rows = new ArrayList();
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] = (pwDsv != null && csvStyleQuoting
&& allQuoted) ? ('"' + m.getColumnLabel(i) + '"')
: 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;
default:
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 TIMESTAMP_WITH_TIMEZONE:
case TIME_WITH_TIMEZONE:
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]
!= TIMESTAMP_WITH_TIMEZONE
&& 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(
SqltoolRB.streamread_failure.getString(),
ioe);
}
stdprintln(SqltoolRB.binbuf_write.getString(
Integer.toString(binBuffer.length),
headerArray[insi],
SqlFile.sqlTypeToString(dataType[insi])
));
if (updateStatus)
shared.userVars.put("?", "");
if (fetchingVar != null) {
shared.userVars.put(fetchingVar, "");
updateUserSettings();
sqlExpandMode = null;
fetchingVar = null;
}
lastSqlStatement = null;
return;
}
if (excludeSysSchemas && val != null && i == 2)
for (String oracleSysSchema : oracleSysSchemas)
if (val.equals(oracleSysSchema)) {
filteredOut = true;
break;
}
// nullRepToken may never be null
if (updateStatus)
shared.userVars.put("?",
((val == null) ? nullRepToken : val));
if (fetchingVar != null) {
if (val == null)
shared.userVars.remove(fetchingVar);
else
shared.userVars.put(fetchingVar, val);
updateUserSettings();
sqlExpandMode = null;
fetchingVar = null;
}
if (silent) {
// N.b. we ignore rows after first row returned
shared.userVars.put("#", "1");
lastSqlStatement = null;
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
? "\u0000" : 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);
}
if (updateStatus && !shared.userVars.containsKey("?"))
shared.userVars.put("?", "");
if (fetchingVar != null) {
shared.userVars.remove(fetchingVar);
updateUserSettings();
sqlExpandMode = null;
fetchingVar = null;
}
shared.userVars.put("#", Integer.toString(rows.size()));
// STEP 2: DISPLAY DATA (= 2a OR 2b)
// STEP 2a (Non-DSV)
if (pwDsv == null) {
condlPrintln("", true);
if (incCount > 1) {
condlPrint(SqlFile.htmlRow(COL_HEAD) + LS + PRE_TD, true);
for (int i = 0; i < headerArray.length; i++) {
condlPrint(""
+ SqlFile.escapeHtml(headerArray[i])
+ " ", true);
condlPrint(((i > 0) ? " " : "")
+ ((i < headerArray.length - 1
|| rightJust[i])
? toPaddedString(
headerArray[i], maxWidth[i],
' ', !rightJust[i])
: headerArray[i])
, false);
}
condlPrintln(LS + PRE_TR + "", true);
condlPrintln("", false);
if (!htmlMode) {
for (int i = 0; i < headerArray.length; i++)
condlPrint(((i > 0) ? " "
: "") + SqlFile.divider(
maxWidth[i]), false);
condlPrintln("", false);
}
}
condlPrintln("", true);
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(""
+ (fieldArray[j].equals("\u0000")
? nullRepHtml
: SqlFile.escapeHtml(fieldArray[j]))
+ " ", true);
condlPrint(((j > 0) ? " " : "")
+ ((j < fieldArray.length - 1
|| rightJust[j])
? toPaddedString(
fieldArray[j], maxWidth[j],
' ', !rightJust[j])
: fieldArray[j])
, false);
}
condlPrintln(LS + PRE_TR + "", true);
condlPrintln("", false);
}
condlPrintln("
", true);
if (interactive && rows.size() != 1)
stdprintln(LS + SqltoolRB.rows_fetched.getString(
rows.size()), 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);
}
if (csvStyleQuoting) {
Pattern delimPat = Pattern.compile(dsvColDelim);
for (String[] fArray : rows)
for (int j = 0; j < fArray.length; j++)
if (fArray[j] != null && (allQuoted
|| fArray[j].indexOf('"') > -1
|| fArray[j].indexOf(dsvColDelim) > -1))
fArray[j] = '"'
+ fArray[j].replace("\"", "\"\"") + '"';
}
for (String[] fArray : rows) {
for (int j = 0; j < fArray.length; j++) {
if (pwDsv == null) 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 :
if (updateStatus)
shared.userVars.put("?", Integer.toString(updateCount));
if (fetchingVar != null) {
shared.userVars.put(
fetchingVar, Integer.toString(updateCount));
fetchingVar = null;
updateUserSettings();
sqlExpandMode = null;
}
if (updateCount != 0 && interactive)
stdprintln((updateCount == 1)
? SqltoolRB.row_update_singular.getString()
: SqltoolRB.row_update_multiple.getString(updateCount));
break;
}
lastSqlStatement = null; // Clear this only if we don't error out
} finally {
if (r != null) try {
r.close();
} catch (SQLException se) {
logger.warning("Failed to close SQL result set: " + se);
} finally {
r = null;
}
if (statement != null) try {
statement.close();
} catch (SQLException se) {
logger.warning("Failed to close SQL statement: " + se);
} finally {
statement = null;
}
}
}
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 <TR> command for the given
* situation.
*
* @param colType Column type: COL_HEAD, COL_ODD or COL_EVEN.
*/
private static String htmlRow(final int colType) {
switch (colType) {
case COL_HEAD :
return PRE_TR + "";
case COL_ODD :
return PRE_TR + " ";
case COL_EVEN :
return PRE_TR + " ";
}
return null;
}
/**
* Returns a divider of hypens of requested length.
*
* @param len Length of output String.
*/
private static String divider(final 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(final 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(final 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));
}
// 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(final Token newBuffer) {
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:
assert false:
"Internal assertion failed. "
+ "Attempted to add command type "
+ newBuffer.getTypeString() + " to buffer";
}
buffer = new Token(newBuffer.type, 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 filterString 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(final String tableName, final String filterString)
throws SQLException {
assert shared.jdbcConn != null:
"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 rows = new ArrayList();
String[] headerArray = {
SqltoolRB.describe_table_name.getString(),
SqltoolRB.describe_table_datatype.getString(),
SqltoolRB.describe_table_width.getString(),
SqltoolRB.describe_table_nonulls.getString(),
SqltoolRB.describe_table_precision.getString(),
SqltoolRB.describe_table_scale.getString(),
};
String[] fieldArray;
int[] maxWidth = { 0, 0, 0, 0, 0, 0 };
boolean[] rightJust = { false, false, true, false, true, true };
int precision, scale;
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));
// 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[6];
precision = m.getPrecision(i + 1);
scale = m.getScale(i + 1);
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)
? "" : "*");
fieldArray[4] =
(precision == 0) ? "" :Integer.toString(precision);
fieldArray[5] = (scale == 0) ? "" :Integer.toString(scale);
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(""
+ LS + SqlFile.htmlRow(COL_HEAD) + LS + PRE_TD, true);
for (int i = 0; i < headerArray.length; i++) {
condlPrint(""
+ SqlFile.escapeHtml(headerArray[i]) + " ", true);
condlPrint(((i > 0) ? " " : "")
+ ((i < headerArray.length - 1 || rightJust[i])
? toPaddedString(
headerArray[i], maxWidth[i], ' ', !rightJust[i])
: headerArray[i])
, false);
}
condlPrintln(LS + PRE_TR + "", true);
condlPrintln("", false);
condlPrintln("", true);
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("" + SqlFile.escapeHtml(fieldArray[j])
+ " ", true);
condlPrint(((j > 0) ? " " : "")
+ ((j < fieldArray.length - 1 || rightJust[j])
? toPaddedString(
fieldArray[j], maxWidth[j], ' ', !rightJust[j])
: fieldArray[j])
, false);
}
condlPrintln(LS + PRE_TR + "", true);
condlPrintln("", false);
}
condlPrintln(LS + "
", true);
} finally {
if (r != null) try {
r.close();
} catch (SQLException nse) {
// intentionally empty;
} finally {
r = null;
}
if (statement != null) try {
statement.close();
} catch (SQLException nse) {
// intentionally empty;
} finally {
statement = null;
}
}
}
/**
* Unset variables are permitted in expressions as long as use
* the short *VARNAME form.
*/
private boolean eval(final 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)];
}
}
if (tokens.length == 1)
return (tokens[0] != null && tokens[0].length() > 0
&& !tokens[0].equals("0")) ^ negate;
if (tokens.length == 3) {
if (tokens[1] == null)
throw new BadSpecial(SqltoolRB.logical_unrecognized.getString());
if (tokens[1].equals("!=") || tokens[1].equals("<>")
|| tokens[1].equals("><")) {
negate = !negate;
tokens[1] = "==";
}
if (tokens[1].equals(">=") || tokens[1].equals("=>")) {
negate = !negate;
tokens[1] = "<";
}
if (tokens[1].equals("<=") || tokens[1].equals("=<")) {
negate = !negate;
tokens[1] = ">";
}
if (tokens[1].equals("==")) {
if (tokens[0] == null || tokens[2] == null)
return (tokens[0] == null && tokens[2] == null) ^ negate;
return tokens[0].equals(tokens[2]) ^ negate;
}
char c1 = (tokens[0] == null || tokens[0].length() < 1)
? '\0' : tokens[0].charAt(0);
char c2 = (tokens[2] == null || tokens[2].length() < 1)
? '\0' : tokens[2].charAt(0);
if (tokens[1].equals(">")) {
if (tokens[0] == null || tokens[2] == null) return !negate;
if (c1 == '-' && c2 == '-') {
negate = !negate;
} else if (c1 == '-') {
return negate;
} else if (c2 == '-') {
return !negate;
}
return (tokens[0].length() > tokens[2].length()
|| ((tokens[0].length() == tokens[2].length())
&& tokens[0].compareTo(tokens[2]) > 0)) ^ negate;
}
if (tokens[1].equals("<")) {
if (tokens[0] == null || tokens[2] == null) return !negate;
if (c1 == '-' && c2 == '-') {
negate = !negate;
} else if (c1 == '-') {
return !negate;
} else if (c2 == '-') {
return negate;
}
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.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.
*
* The condlPrint methods do not escape HTML like the stdprint methods do.
*/
private void condlPrintln(final String s, final 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.
*
* The condlPrint methods do not escape HTML like the stdprint methods do.
*/
private void condlPrint(final String s, final 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(final Map, ?> map, final 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