All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hsqldb.cmdline.SqlFile Maven / Gradle / Ivy

There is a newer version: 2.7.2
Show newest version
/* Copyright (c) 2001-2019, 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.lib.AppendableException;
import org.hsqldb.lib.FrameworkLogger;
import org.hsqldb.lib.RCData;
import org.hsqldb.lib.StringUtil;

/* $Id: SqlFile.java 5995 2019-06-01 19:06:14Z 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: 5995 $, $Date: 2019-06-01 15:06:14 -0400 (Sat, 01 Jun 2019) $ * @author Blaine Simpson (blaine dot simpson at admc dot com) */ public class SqlFile { 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: 5995 $"; 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) */ 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, ".") : atBase; } 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 */ 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 org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE: case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE: case java.sql.Types.TIMESTAMP: case java.sql.Types.DATE: case java.sql.Types.TIME: ts = r.getTimestamp(i); isValNull = r.wasNull(); val = ((ts == null) ? null : ts.toString()); // Following block truncates non-zero // sub-seconds from time types OTHER than // TIMESTAMP. if (dataType[insi] != java.sql.Types.TIMESTAMP && dataType[insi] != org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE && val != null) { dotAt = val.lastIndexOf('.'); for (int z = dotAt + 1; z < val.length(); z++) if (val.charAt(z) != '0') { dotAt = 0; break; } if (dotAt > 1) val = val.substring(0, dotAt); } break; default: val = r.getString(i); isValNull = r.wasNull(); // If we tried to get a String but it // failed, try getting it with a String // Stream if (val == null) try { val = streamToString( r.getAsciiStream(i), shared.encoding); isValNull = r.wasNull(); } catch (Exception e) { // This isn't an error. // We are attempting to do a stream // fetch if-and-only-if the column // supports it. } } } if (binary || (val == null && !isValNull)) { if (pwDsv != null) throw new SqlToolError( SqltoolRB.dsv_bincol.getString()); // DB has a value but we either explicitly want // it as binary, or we failed to get it as String. try { binBuffer = SqlFile.streamToBytes(r.getBinaryStream(i)); isValNull = r.wasNull(); } catch (IOException ioe) { throw new SqlToolError( 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("", true); condlPrint(((i > 0) ? " " : "") + ((i < headerArray.length - 1 || rightJust[i]) ? StringUtil.toPaddedString( headerArray[i], maxWidth[i], ' ', !rightJust[i]) : headerArray[i]) , false); } condlPrintln(LS + PRE_TR + "", 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]) ? StringUtil.toPaddedString( fieldArray[j], maxWidth[j], ' ', !rightJust[j]) : fieldArray[j]) , false); } condlPrintln(LS + PRE_TR + "", true); condlPrintln("", false); } condlPrintln("
" + SqlFile.escapeHtml(headerArray[i]) + "
", 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 filter Optional regex to filter by. * By default, will match only against the column name. * Prefix with "/" to match against the entire output line. */ private void describe(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("", true); condlPrint(((i > 0) ? " " : "") + ((i < headerArray.length - 1 || rightJust[i]) ? StringUtil.toPaddedString( headerArray[i], maxWidth[i], ' ', !rightJust[i]) : headerArray[i]) , false); } condlPrintln(LS + PRE_TR + "", 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]) ? StringUtil.toPaddedString( fieldArray[j], maxWidth[j], ' ', !rightJust[j]) : fieldArray[j]) , false); } condlPrintln(LS + PRE_TR + "", true); condlPrintln("", false); } condlPrintln(LS + "
" + SqlFile.escapeHtml(headerArray[i]) + "
", 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 entry : new TreeMap(map).entrySet()) { s = (String) entry.getValue(); SqlFile.appendLine(sb, " " + (String) entry.getKey() + ": " + (withValues ? ("(" + s + ')') : Integer.toString( s.length()))); } return sb.toString(); } /** * Ascii file dump. * * dumpFile must not be null. */ private void dump(final String varName, final File dumpFile) throws IOException, BadSpecial { String val = shared.userVars.get(varName); if (val == null) throw new BadSpecial(SqltoolRB.plvar_undefined.getString(varName)); OutputStreamWriter osw = new OutputStreamWriter( new FileOutputStream(dumpFile), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding); try { osw.write(val); if (val.length() > 0) { char lastChar = val.charAt(val.length() - 1); if (lastChar != '\n' && lastChar != '\r') osw.write(LS); } osw.flush(); } finally { try { osw.close(); } catch (IOException ioe) { // Intentionally empty } finally { osw = null; // Encourage GC of buffers } } // Since opened in overwrite mode, since we didn't exception out, // we can be confident that we wrote all the bytest in the file. stdprintln(SqltoolRB.file_wrotechars.getString( Long.toString(dumpFile.length()), dumpFile.toString())); } byte[] binBuffer; /** * Binary file dump * * dumpFile must not be null. */ private void dump(final File dumpFile) throws IOException, BadSpecial { if (binBuffer == null) throw new BadSpecial(SqltoolRB.binbuffer_empty.getString()); int len = 0; FileOutputStream fos = new FileOutputStream(dumpFile); try { fos.write(binBuffer); len = binBuffer.length; binBuffer = null; fos.flush(); } finally { try { fos.close(); } catch (IOException ioe) { // Intentionally empty } finally { fos = null; // Encourage GC of buffers } } stdprintln(SqltoolRB.file_wrotechars.getString( len, dumpFile.toString())); } /** * As the name says... * * This method always closes the input stream. * * @param is InputStream * @param cs String * @throws IOException on read errors * @return String */ public String streamToString(InputStream is, final String cs) throws IOException { byte[] ba = null; int bytesread = 0; int retval; try { try { ba = new byte[is.available()]; } catch (RuntimeException re) { throw new IOException(SqltoolRB.read_toobig.getString()); } while (bytesread < ba.length && (retval = is.read( ba, bytesread, ba.length - bytesread)) > 0) { bytesread += retval; } if (bytesread != ba.length) throw new IOException( SqltoolRB.read_partial.getString(bytesread, ba.length)); try { return (cs == null) ? (new String(ba)) : (new String(ba, cs)); } catch (UnsupportedEncodingException uee) { throw new IOException( SqltoolRB.encode_fail.getString(uee)); } catch (RuntimeException re) { throw new IOException(SqltoolRB.read_convertfail.getString()); } } finally { try { is.close(); } catch (IOException ioe) { // intentionally empty } finally { is = null; // Encourage GC of buffers. // Modification of input param will elicit a compiler warning. // N.b. the caller reference will remain non-null. } } } /** * Ascii file load. */ private void load(final String varName, final URL asciiUrl, final String cs) throws IOException { String string = streamToString(asciiUrl.openStream(), cs); // The streamToString() method ensures that the Stream gets closed shared.userVars.put(varName, string); if (!varPattern.matcher(varName).matches()) errprintln(SqltoolRB.varname_warning.getString(varName)); updateUserSettings(); sqlExpandMode = null; } /** * As the name says... * * @param is input * @throws IOException on read errors * @return byte[] */ public static byte[] streamToBytes(final InputStream is) throws IOException { byte[] xferBuffer = new byte[10240]; byte[] outBytes = null; int i; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { while ((i = is.read(xferBuffer)) > 0) baos.write(xferBuffer, 0, i); outBytes = baos.toByteArray(); } finally { baos = null; // Encourage buffer GC } return outBytes; } /** * Legacy function.. * See following function for documentatin. */ public static byte[] loadBinary(final File binFile) throws IOException { return loadBinary(new URL("file", null, binFile.getPath())); } /** * Binary file load * * @return The bytes which are the content of the file * @throws IOException on read errors * @param binUrl url */ public static byte[] loadBinary(final URL binUrl) throws IOException { byte[] xferBuffer = new byte[10240]; byte[] outBytes = null; ByteArrayOutputStream baos; int i; InputStream fis = binUrl.openStream(); try { baos = new ByteArrayOutputStream(); while ((i = fis.read(xferBuffer)) > 0) baos.write(xferBuffer, 0, i); outBytes = baos.toByteArray(); } finally { try { fis.close(); } catch (IOException ioe) { // intentionally empty } finally { fis = null; // Encourage GC of buffers baos = null; // Encourage GC of buffers } } return outBytes; } /** * This method is used to tell SqlFile whether this Sql Type must * ALWAYS be loaded to the binary buffer without displaying. *

* N.b.: If this returns "true" for a type, then the user can never * "see" values for these columns. * Therefore, if a type may-or-may-not-be displayable, better to return * false here and let the user choose. * In general, if there is a toString() operator for this Sql Type * then return false, since the JDBC driver should know how to make the * value displayable. *

* * @param i JDBC type number * @return boolean * @see http://java.sun.com/docs/books/tutorial/jdbc/basics/retrieving.html * The table on this page lists the most common SqlTypes, all of which * must implement toString() * @see java.sql.Types */ public static boolean canDisplayType(final int i) { /* I don't now about some of the more obscure types, like REF and * DATALINK */ switch (i) { //case java.sql.Types.BINARY : case java.sql.Types.BLOB : case java.sql.Types.JAVA_OBJECT : //case java.sql.Types.LONGVARBINARY : //case java.sql.Types.LONGVARCHAR : case java.sql.Types.OTHER : case java.sql.Types.STRUCT : //case java.sql.Types.VARBINARY : return false; } return true; } // won't compile with JDK 1.4 without these private static final int JDBC3_BOOLEAN = 16; private static final int JDBC3_DATALINK = 70; /** * Return a String representation of the specified java.sql.Types type. * * @param i int * @return String */ public static String sqlTypeToString(final int i) { switch (i) { case java.sql.Types.ARRAY : return "ARRAY"; case java.sql.Types.BIGINT : return "BIGINT"; case java.sql.Types.BINARY : return "BINARY"; case java.sql.Types.BIT : return "BIT"; case java.sql.Types.BLOB : return "BLOB"; case JDBC3_BOOLEAN : return "BOOLEAN"; case java.sql.Types.CHAR : return "CHAR"; case java.sql.Types.CLOB : return "CLOB"; case JDBC3_DATALINK : return "DATALINK"; case java.sql.Types.DATE : return "DATE"; case java.sql.Types.DECIMAL : return "DECIMAL"; case java.sql.Types.DISTINCT : return "DISTINCT"; case java.sql.Types.DOUBLE : return "DOUBLE"; case java.sql.Types.FLOAT : return "FLOAT"; case java.sql.Types.INTEGER : return "INTEGER"; case java.sql.Types.JAVA_OBJECT : return "JAVA_OBJECT"; case java.sql.Types.LONGVARBINARY : return "LONGVARBINARY"; case java.sql.Types.LONGVARCHAR : return "LONGVARCHAR"; case java.sql.Types.NULL : return "NULL"; case java.sql.Types.NUMERIC : return "NUMERIC"; case java.sql.Types.OTHER : return "OTHER"; case java.sql.Types.REAL : return "REAL"; case java.sql.Types.REF : return "REF"; case java.sql.Types.SMALLINT : return "SMALLINT"; case java.sql.Types.STRUCT : return "STRUCT"; case java.sql.Types.TIME : return "TIME"; case java.sql.Types.TIMESTAMP : return "TIMESTAMP"; case java.sql.Types.TINYINT : return "TINYINT"; case java.sql.Types.VARBINARY : return "VARBINARY"; case java.sql.Types.VARCHAR : return "VARCHAR"; case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE : return "SQL_TIME_WITH_TIME_ZONE"; case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE : return "SQL_TIMESTAMP_WITH_TIME_ZONE"; } return "Unknown type " + i; } /** * Validate that String is safe to write TO DSV file. * * @throws SqlToolError if validation fails. * @param s String */ public void dsvSafe(final String s) throws SqlToolError { assert pwDsv != null && dsvColDelim != null && dsvRowDelim != null && nullRepToken != null: "Assertion failed. \n" + "dsvSafe called when DSV settings are incomplete"; if (s == null) return; if (s.indexOf(dsvColDelim) > 0) throw new SqlToolError( SqltoolRB.dsv_coldelim_present.getString(dsvColDelim)); if (s.indexOf(dsvRowDelim) > 0) throw new SqlToolError( SqltoolRB.dsv_rowdelim_present.getString(dsvRowDelim)); if (s.trim().equals(nullRepToken)) // The trim() is to avoid the situation where the contents of a // field "looks like" the null-rep token. throw new SqlToolError( SqltoolRB.dsv_nullrep_present.getString(nullRepToken)); } /** * Translates user-supplied escapes into the traditionaly corresponding * corresponding binary characters. * * Allowed sequences: *
    *
  • \0\d+ (an octal digit) *
  • \[0-9]\d* (a decimal digit) *
  • \[Xx][0-9]{2} (a hex digit) *
  • \n Newline (Ctrl-J) *
  • \r Carriage return (Ctrl-M) *
  • \t Horizontal tab (Ctrl-I) *
  • \f Form feed (Ctrl-L) *
* * Java 1.4 String methods will make this into a 1 or 2 line task. * * @param inString input string * @return converted string */ public static String convertEscapes(final String inString) { if (inString == null) return null; return convertNumericEscapes( convertEscapes(convertEscapes(convertEscapes(convertEscapes( convertEscapes(inString, "\\n", "\n"), "\\r", "\r"), "\\t", "\t"), "\\\\", "\\"), "\\f", "\f") ); } /** * @param string Non-null String to modify. */ private static String convertNumericEscapes(final String string) { String workString = string; int i = 0; for (char dig = '0'; dig <= '9'; dig++) { while ((i = workString.indexOf("\\" + dig, i)) > -1 && i < workString.length() - 1) workString = convertNumericEscape(string, i); while ((i = workString.indexOf("\\x" + dig, i)) > -1 && i < workString.length() - 1) workString = convertNumericEscape(string, i); while ((i = workString.indexOf("\\X" + dig, i)) > -1 && i < workString.length() - 1) workString = convertNumericEscape(string, i); } return workString; } /** * @offset Position of the leading \. */ private static String convertNumericEscape( final String string, final int offset) { int post = -1; int firstDigit = -1; int radix = -1; if (Character.toUpperCase(string.charAt(offset + 1)) == 'X') { firstDigit = offset + 2; radix = 16; post = firstDigit + 2; if (post > string.length()) post = string.length(); } else { firstDigit = offset + 1; radix = (Character.toUpperCase(string.charAt(firstDigit)) == '0') ? 8 : 10; post = firstDigit + 1; while (post < string.length() && Character.isDigit(string.charAt(post))) post++; } return string.substring(0, offset) + ((char) Integer.parseInt(string.substring(firstDigit, post), radix)) + string.substring(post); } /** * * @param string Non-null String to modify. * @param from String * @param to String * @return String */ private static String convertEscapes( final String string, final String from, final String to) { String workString = string; int i = 0; int fromLen = from.length(); while ((i = workString.indexOf(from, i)) > -1 && i < workString.length() - 1) workString = workString.substring(0, i) + to + workString.substring(i + fromLen); return workString; } private void checkFor02(final String s) throws SqlToolError { try { if (csvStyleQuoting && s.indexOf('\u0002') > -1) throw new SqlToolError( SqltoolRB.csv_coldelim_present.getString("\\u0002")); } catch (RuntimeException re) { throw new SqlToolError(SqltoolRB.read_convertfail.getString(), re); } } /** * Convert CSV 'line' into DSV 'lines'. * * @param s String * @param lineNum int * @throws SqlToolError * @return String */ private String preprocessCsvQuoting(final String s, final int lineNum) throws SqlToolError { StringBuilder sb = new StringBuilder(); int offset, segLen, prevOffset; if (s.indexOf('"') < 0) return s.replaceAll(dsvColSplitter, "\u0002"); prevOffset = -1; SEEK_QUOTEDFIELD: while (prevOffset < s.length() - 1) { // Get start of next quoted field: offset = s.indexOf('"', prevOffset + 1); segLen = ((offset < 0) ? s.length() : offset) - (prevOffset + 1); if (segLen > 0) // Here we insert non-quoted segments, replacing all // dsvColSplitters. sb.append(s.substring( prevOffset + 1, prevOffset + 1 + segLen) .replaceAll(dsvColSplitter, "\u0002")); if (offset < 0) break; // Done with line prevOffset = offset; while ((offset = s.indexOf( '"', prevOffset + 1)) > -1) { if (offset - prevOffset > 1) // Here we insert quoted segments without any "s. sb.append(s.substring( prevOffset + 1, offset)); prevOffset = offset; if (s.length() < offset + 2 || s.charAt(offset + 1) != '"') // Field terminated continue SEEK_QUOTEDFIELD; // Field-internal "" prevOffset++; sb.append('"'); } throw new SqlToolError( SqltoolRB.csv_quote_unterminated.getString(lineNum)); } return sb.toString(); } /** * Name is self-explanatory. * * @param fileUrl String * @param skipPrefix String * @throws SqlToolError Would prefer to throw an internal exception, * but we want this method to have external * visibility. */ public void importDsv(final URL fileUrl, final String skipPrefix) throws SqlToolError { /* * If there is user demand, open file in random access mode so don't * need to load 2 copies of the entire file into memory. * This will be difficult because can't use standard Java language * features to search through a character array for multi-character * substrings. */ requireConnection(); /* To make string comparisons, contains() methods, etc. a little * simpler and concise, just switch all column names to lower-case. * This is ok since we acknowledge up front that DSV import/export * assume no special characters or escaping in column names. */ if (csvStyleQuoting && (dsvColSplitter.indexOf('"') > -1 || dsvRowSplitter.indexOf('"') > -1)) throw new SqlToolError(SqltoolRB.dsv_q_nodblquote.getString()); Matcher matcher; SortedMap constColMap = null; if (dsvConstCols != null) { // We trim col. names, but not values. Must allow users to // specify values as spaces, empty string, null. // We do not support CVS-quoted style constColMap String. // Must be specified in DSV style. constColMap = new TreeMap(); for (String constPair : dsvConstCols.split(dsvColSplitter, -1)) { matcher = nameValPairPattern.matcher(constPair); if (!matcher.matches()) throw new SqlToolError( SqltoolRB.dsv_constcols_nullcol.getString()); constColMap.put(matcher.group(1).toLowerCase(), ((matcher.groupCount() < 2 || matcher.group(2) == null) ? "" : matcher.group(2))); } } Set skipCols = null; if (dsvSkipCols != null) { // We do not support CVS-quoted style skipCols String. // Must be specified in DSV style. skipCols = new HashSet(); for (String skipCol : dsvSkipCols.split(dsvColSplitter, -1)) skipCols.add(skipCol.trim().toLowerCase()); } FileRecordReader dsvReader = null; try { dsvReader = new FileRecordReader(fileUrl, dsvRowSplitter, (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding); } catch (UnsupportedEncodingException uee) { throw new SqlToolError(uee); } catch (IOException ioe) { throw new SqlToolError(SqltoolRB.file_readfail.getString( fileUrl.toString())); } catch (PatternSyntaxException pse) { throw new SqlToolError( SqltoolRB.regex_malformat.getString(dsvRowSplitter)); } int retval; String dateString; List headerList = new ArrayList(); String tableName = dsvTargetTable; // First read one until we get one header line int lineCount = 0; String trimmedLine = null; boolean switching = false; int headerOffset = 0; // Used to offset read-start of header record String curLine = null; // Val will be replaced 4 lines down // This is just to quiet compiler warning try { while (true) { try { curLine = dsvReader.nextRecord(); } catch (IOException ioe) { throw new SqlToolError(ioe); } if (curLine == null) throw new SqlToolError(SqltoolRB.dsv_header_none.getString()); checkFor02(curLine); lineCount++; trimmedLine = curLine.trim(); if (trimmedLine.length() < 1 || (skipPrefix != null && trimmedLine.startsWith(skipPrefix))) continue; if (trimmedLine.startsWith("targettable=")) { if (tableName == null) tableName = trimmedLine.substring( "targettable=".length()).trim(); continue; } if (trimmedLine.equals("headerswitch{")) { if (tableName == null) throw new SqlToolError( SqltoolRB.dsv_header_noswitchtarg.getString( lineCount)); switching = true; continue; } if (trimmedLine.equals("}")) throw new SqlToolError( SqltoolRB.dsv_header_noswitchmatch.getString(lineCount)); if (!switching) break; int colonAt = trimmedLine.indexOf(':'); if (colonAt < 1 || colonAt == trimmedLine.length() - 1) throw new SqlToolError( SqltoolRB.dsv_header_nonswitched.getString(lineCount)); String headerName = trimmedLine.substring(0, colonAt).trim(); // Need to be sure here that tableName is not null (in // which case it would be determined later on by the file name). if (headerName.equals("*") || headerName.equalsIgnoreCase(tableName)){ headerOffset = 1 + curLine.indexOf(':'); break; } // Skip non-matched header line } if (csvStyleQuoting) curLine = preprocessCsvQuoting(curLine, lineCount); String headerLine = curLine.substring(headerOffset); String colName; String[] cols = headerLine.split( (csvStyleQuoting ? "\u0002" : dsvColSplitter), -1); Set usedCols = new HashSet(); // Checks for dup cols for (String col : cols) { if (col.length() < 1) throw new SqlToolError(SqltoolRB.dsv_nocolheader.getString( headerList.size() + 1, lineCount)); colName = col.trim().toLowerCase(); if (colName.equals("-") || (skipCols != null && skipCols.remove(colName)) || (constColMap != null && constColMap.containsKey(colName))) colName = null; headerList.add(colName); if (colName == null) continue; if (usedCols.contains(colName.toLowerCase())) throw new SqlToolError( SqltoolRB.import_col_dup.getString(colName)); usedCols.add(colName.toLowerCase()); } if (skipCols != null && skipCols.size() > 0) throw new SqlToolError(SqltoolRB.dsv_skipcols_missing.getString( skipCols.toString())); boolean oneCol = false; // At least 1 non-null column for (String header : headerList) if (header != null) { oneCol = true; break; } if (oneCol == false) // Difficult call, but I think in any real-world situation, the // user will want to know if they are inserting records with no // data from their input file. throw new SqlToolError( SqltoolRB.dsv_nocolsleft.getString(dsvSkipCols)); int inputColHeadCount = headerList.size(); if (constColMap != null) headerList.addAll(constColMap.keySet()); String[] headers = headerList.toArray(new String[0]); // headers contains input headers + all constCols, some of these // values may be nulls. if (tableName == null) { tableName = dsvReader.getName(); int i = tableName.lastIndexOf('.'); if (i > 0) tableName = tableName.substring(0, i); } StringBuffer tmpSb = new StringBuffer(); List tmpList = new ArrayList(); int skippers = 0; for (String header : headers) { if (header == null) { skippers++; continue; } if (tmpSb.length() > 0) tmpSb.append(", "); tmpSb.append(header); tmpList.add(header); } boolean[] autonulls = new boolean[headers.length - skippers]; boolean[] parseDate = new boolean[autonulls.length]; boolean[] parseBool = new boolean[autonulls.length]; char[] readFormat = new char[autonulls.length]; String[] insertFieldName = tmpList.toArray(new String[] {}); // Remember that the headers array has all columns in DSV file, // even skipped columns. // The autonulls array only has columns that we will insert into. StringBuffer sb = new StringBuffer("INSERT INTO " + tableName + " (" + tmpSb + ") VALUES ("); StringBuffer typeQuerySb = new StringBuffer("SELECT " + tmpSb + " FROM " + tableName + " WHERE 1 = 2"); try { // THIS THROWING!!!!: ResultSetMetaData rsmd = shared.jdbcConn.createStatement().executeQuery( typeQuerySb.toString()).getMetaData(); if (rsmd.getColumnCount() != autonulls.length) { System.err.println("C23"); throw new SqlToolError( SqltoolRB.dsv_metadata_mismatch.getString()); } // Don't know if it's possible to get here. // If so, it's probably a SqlTool problem, not a user or // data problem. // Should be researched and either return a user-friendly // message or a RuntimeExceptin. for (int i = 0; i < autonulls.length; i++) { autonulls[i] = true; parseDate[i] = false; parseBool[i] = false; readFormat[i] = 's'; // regular Strings switch(rsmd.getColumnType(i + 1)) { case java.sql.Types.BIT : autonulls[i] = true; readFormat[i] = 'b'; break; case java.sql.Types.LONGVARBINARY : case java.sql.Types.VARBINARY : case java.sql.Types.BINARY : autonulls[i] = true; readFormat[i] = 'x'; break; case java.sql.Types.BOOLEAN: parseBool[i] = true; break; case java.sql.Types.ARRAY : autonulls[i] = true; readFormat[i] = 'a'; break; case java.sql.Types.VARCHAR : case java.sql.Types.BLOB : case java.sql.Types.CLOB : case java.sql.Types.LONGVARCHAR : autonulls[i] = false; // This means to preserve white space and to insert // "" for "". Otherwise we trim white space and // insert null for \s*. break; case java.sql.Types.DATE: case java.sql.Types.TIME: case java.sql.Types.TIMESTAMP: case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE: case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE: parseDate[i] = true; break; default: break; } } } catch (SQLException se) { throw new SqlToolError(SqltoolRB.query_metadatafail.getString( typeQuerySb.toString()), se); } for (int i = 0; i < autonulls.length; i++) { if (i > 0) sb.append(", "); sb.append('?'); } // Initialize REJECT file(s) int rejectCount = 0; File rejectFile = null; File rejectReportFile = null; PrintWriter rejectWriter = null; PrintWriter rejectReportWriter = null; try { if (dsvRejectFile != null) try { rejectFile = dereferenceAtToFile(dsvRejectFile); rejectWriter = new PrintWriter( new OutputStreamWriter(new FileOutputStream(rejectFile), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding)); rejectWriter.print(headerLine + dsvRowDelim); } catch (BadSpecial bs) { throw new SqlToolError(SqltoolRB.dsv_rejectfile_setupfail.getString( dsvRejectFile), bs); } catch (IOException ioe) { throw new SqlToolError(SqltoolRB.dsv_rejectfile_setupfail.getString( dsvRejectFile), ioe); } if (dsvRejectReport != null) try { rejectReportFile = dereferenceAtToFile(dsvRejectReport); rejectReportWriter = new PrintWriter(new OutputStreamWriter( new FileOutputStream(rejectReportFile), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding)); boolean setTitle = !shared.userVars.containsKey("REPORT_TITLE"); if (setTitle) shared.userVars.put("REPORT_TITLE", "SqlTool " + (csvStyleQuoting ? "CSV" : "DSV") + " Reject Report"); try { writeHeader(rejectReportWriter, dsvRejectReport); } finally { if (setTitle) shared.userVars.remove("REPORT_TITLE"); } rejectReportWriter.println(SqltoolRB.rejectreport_top.getString( dsvReader.toString(), ((rejectFile == null) ? SqltoolRB.none.getString() : rejectFile.getPath()), ((rejectFile == null) ? null : rejectFile.getPath()))); } catch (BadSpecial bs) { throw new SqlToolError( SqltoolRB.dsv_rejectreport_setupfail.getString( dsvRejectReport), bs); } catch (IOException ioe) { throw new SqlToolError( SqltoolRB.dsv_rejectreport_setupfail.getString( dsvRejectReport), ioe); } int recCount = 0; int skipCount = 0; PreparedStatement ps = null; boolean importAborted = false; boolean doResetAutocommit = false; try { doResetAutocommit = dsvRecordsPerCommit > 0 && shared.jdbcConn.getAutoCommit(); if (doResetAutocommit) shared.jdbcConn.setAutoCommit(false); } catch (SQLException se) { throw new SqlToolError( SqltoolRB.rpc_autocommit_failure.getString(), se); } // We're now assured that if dsvRecordsPerCommit is > 0, then // autocommit is off. try { try { ps = shared.jdbcConn.prepareStatement(sb.toString() + ')'); } catch (SQLException se) { throw new SqlToolError( SqltoolRB.insertion_preparefail.getString( sb.toString()), se); } String[] dataVals = new String[autonulls.length]; // Length is number of cols to insert INTO, not nec. # in DSV file. int readColCount; int storeColCount; Matcher arMatcher; String currentFieldName = null; String[] arVals; // Insert data rows 1-row-at-a-time while (true) try { try { try { curLine = dsvReader.nextRecord(); } catch (IOException ioe) { throw new SqlToolError(ioe); } if (curLine == null) break; checkFor02(curLine); if (csvStyleQuoting) curLine = preprocessCsvQuoting(curLine, ++lineCount); trimmedLine = curLine.trim(); if (trimmedLine.length() < 1) continue; // Silently skip blank lines if (skipPrefix != null && trimmedLine.startsWith(skipPrefix)) { skipCount++; continue; } if (switching) { if (trimmedLine.equals("}")) { switching = false; continue; } int colonAt = trimmedLine.indexOf(':'); if (colonAt < 1 || colonAt == trimmedLine.length() - 1) throw new SqlToolError( SqltoolRB.dsv_header_matchernonhead.getString( lineCount)); continue; } // Finished using "trimmed" line now. Whitespace is // meaningful hereafter. // Finally we will attempt to add a record! recCount++; // Remember that recCount counts both inserts + rejects readColCount = 0; storeColCount = 0; cols = curLine.split( (csvStyleQuoting ? "\u0002" : dsvColSplitter), -1); for (String col : cols) { if (readColCount == inputColHeadCount) throw new RowError( SqltoolRB.dsv_colcount_mismatch.getString( inputColHeadCount, 1 + readColCount)); if (headers[readColCount++] != null) dataVals[storeColCount++] = dsvTrimAll ? col.trim() : col; } if (readColCount < inputColHeadCount) throw new RowError( SqltoolRB.dsv_colcount_mismatch.getString( inputColHeadCount, readColCount)); /* Already checked for readColCount too high in prev. block */ if (constColMap != null) for (String val : constColMap.values()) dataVals[storeColCount++] = val; if (storeColCount != dataVals.length) throw new RowError( SqltoolRB.dsv_insertcol_mismatch.getString( dataVals.length, storeColCount)); for (int i = 0; i < dataVals.length; i++) { currentFieldName = insertFieldName[i]; if (autonulls[i]) dataVals[i] = dataVals[i].trim(); // N.b. WE SPECIFICALLY DO NOT HANDLE TIMES WITHOUT // DATES, LIKE "3:14:00", BECAUSE, WHILE THIS MAY BE // USEFUL AND EFFICIENT, IT IS NOT PORTABLE. //System.err.println("ps.setString(" + i + ", " // + dataVals[i] + ')'); if (parseDate[i]) { if ((dataVals[i].length() < 1 && autonulls[i]) || dataVals[i].equals(nullRepToken)) { ps.setTimestamp(i + 1, null); } else { // BEWARE: This may not work for some non-US // date/time formats. if (dataVals[i].indexOf(':') > 0 && dataVals[i].indexOf('-') > 0) { dateString = dataVals[i]; } else if (dataVals[i].indexOf(':') < 1) { dateString = dataVals[i] + " 0:00:00"; } else if (dataVals[i].indexOf('-') < 1) { dateString = "0000-00-00 " + dataVals[i]; } else { dateString = null; // To shut up compiler assert false: "Unexpected date/time val: " + dataVals[i]; } try { ps.setTimestamp(i + 1, java.sql.Timestamp.valueOf(dateString)); } catch (IllegalArgumentException iae) { throw new RowError( SqltoolRB.time_bad.getString( dateString), iae); } } } else if (parseBool[i]) { if ((dataVals[i].length() < 1 && autonulls[i]) || dataVals[i].equals(nullRepToken)) { ps.setNull(i + 1, java.sql.Types.BOOLEAN); } else { try { ps.setBoolean(i + 1, Boolean.parseBoolean(dataVals[i])); // Boolean... is equivalent to Java 4's // Boolean.parseBoolean(). } catch (IllegalArgumentException iae) { throw new RowError( SqltoolRB.boolean_bad.getString( dataVals[i]), iae); } } } else { switch (readFormat[i]) { case 'b': ps.setBytes( i + 1, (dataVals[i].length() < 1) ? null : SqlFile.bitCharsToBytes( dataVals[i])); break; case 'x': ps.setBytes( i + 1, (dataVals[i].length() < 1) ? null : SqlFile.hexCharOctetsToBytes( dataVals[i])); break; case 'a' : if (SqlFile.createArrayOfMethod == null) throw new SqlToolError( SqltoolRB.arrayimp_jvmreq .getString()); if (dataVals[i].length() < 1) { ps.setArray(i + 1, null); break; } arMatcher = arrayPattern.matcher(dataVals[i]); if (!arMatcher.matches()) throw new RowError( SqltoolRB.arrayval_malformat .getString(dataVals[i])); arVals = (arMatcher.group(1) == null) ? (new String[0]) : arMatcher.group(1).split("\\s*,\\s*"); // N.b. THIS DOES NOT HANDLE commas WITHIN // Array ELEMENT VALUES. try { ps.setArray(i + 1, (java.sql.Array) SqlFile.createArrayOfMethod.invoke( shared.jdbcConn, "VARCHAR", arVals)); } catch (IllegalAccessException iae) { throw new RuntimeException(iae); } catch (InvocationTargetException ite) { if (ite.getCause() != null && ite.getCause() instanceof AbstractMethodError) throw new SqlToolError( SqltoolRB.sqlarray_badjvm .getString()); throw new RuntimeException(ite); } // createArrayOf method is Java-6-specific! break; default: ps.setString( i + 1, (((dataVals[i].length() < 1 && autonulls[i]) || dataVals[i].equals(nullRepToken)) ? null : dataVals[i])); } } currentFieldName = null; } retval = ps.executeUpdate(); if (retval != 1) throw new RowError( SqltoolRB.inputrec_modified.getString(retval)); if (dsvRecordsPerCommit > 0 && (recCount - rejectCount) % dsvRecordsPerCommit == 0) { shared.jdbcConn.commit(); shared.possiblyUncommitteds = false; } else { shared.possiblyUncommitteds = true; } } catch (NumberFormatException nfe) { throw new RowError(null, nfe); } catch (SQLException se) { throw new RowError(null, se); } } catch (RowError re) { rejectCount++; if (rejectWriter != null || rejectReportWriter != null) { if (rejectWriter != null) rejectWriter.print(curLine + dsvRowDelim); if (rejectReportWriter != null) genRejectReportRecord(rejectReportWriter, rejectCount, lineCount, currentFieldName, re.getMessage(), re.getCause()); } else { importAborted = true; throw new SqlToolError( SqltoolRB.dsv_recin_fail.getString( lineCount, currentFieldName) + ((re.getMessage() == null) ? "" : (" " + re.getMessage())), re.getCause()); } } } finally { if (ps != null) try { ps.close(); } catch (SQLException se) { // We already got what we want from it, or have/are // processing a more specific error. } finally { ps = null; // Encourage GC of buffers } try { if (dsvRecordsPerCommit > 0 && (recCount - rejectCount) % dsvRecordsPerCommit != 0) { // To be consistent, if *DSV_RECORDS_PER_COMMIT is set, we // always commit all inserted records. // This little block commits any straggler commits since the // last commit. shared.jdbcConn.commit(); shared.possiblyUncommitteds = false; } if (doResetAutocommit) shared.jdbcConn.setAutoCommit(true); } catch (SQLException se) { throw new SqlToolError( SqltoolRB.rpc_commit_failure.getString(), se); } String summaryString = null; if (recCount > 0) { summaryString = SqltoolRB.dsv_import_summary.getString( ((skipPrefix == null) ? "" : ("'" + skipPrefix + "'-")), Integer.toString(skipCount), Integer.toString(rejectCount), Integer.toString(recCount - rejectCount), (importAborted ? "importAborted" : null)); stdprintln(summaryString); } try { if (recCount > rejectCount && dsvRecordsPerCommit < 1 && !shared.jdbcConn.getAutoCommit()) stdprintln(SqltoolRB.insertions_notcommitted.getString()); } catch (SQLException se) { stdprintln(SqltoolRB.autocommit_fetchfail.getString()); stdprintln(SqltoolRB.insertions_notcommitted.getString()); // No reason to throw here. If user attempts to use the // connection for anything significant, we will throw then. } if (rejectWriter != null) rejectWriter.flush(); if (rejectReportWriter != null && rejectCount > 0) { rejectReportWriter.println( SqltoolRB.rejectreport_bottom.getString( summaryString, revnum)); writeFooter(rejectReportWriter, dsvRejectReport); rejectReportWriter.flush(); } } } finally { if (rejectWriter != null) try { rejectWriter.close(); } finally { rejectWriter = null; // Encourage GC of buffers } if (rejectReportWriter != null) try { rejectReportWriter.close(); } finally { rejectReportWriter = null; // Encourage GC of buffers } if (rejectCount == 0) { if (rejectFile != null && rejectFile.exists() && !rejectFile.delete()) errprintln(SqltoolRB.dsv_rejectfile_purgefail.getString( rejectFile.toString())); if (rejectReportFile != null && !rejectReportFile.delete()) errprintln(SqltoolRB.dsv_rejectreport_purgefail.getString( rejectReportFile.toString())); // These are trivial errors. } } } finally { if (dsvReader.isOpen()) try { dsvReader.close(); } catch (Exception ioe) { // Just log it logger.error( SqltoolRB.inputfile_closefail.getString() + ": " + ioe); } } } protected static void appendLine(final StringBuffer sb, final String s) { sb.append(s + LS); } /** * Does a poor-man's parse of a MSDOS command line and parses it into a * WIndows cmd.exe invocation to approximate. * * @param monolithic String * @return String[] */ private static String[] genWinArgs(final String monolithic) { List list = new ArrayList(); list.add("cmd.exe"); list.add("/y"); list.add("/c"); Matcher m = wincmdPattern.matcher(monolithic); while (m.find()) for (int i = 1; i <= m.groupCount(); i++) { if (m.group(i) == null) continue; if (m.group(i).length() > 1 && m.group(i).charAt(0) == '"') { list.add(m.group(i).substring(1, m.group(i).length() - 1)); continue; } list.addAll(Arrays.asList(m.group(i).split("\\s+", -1))); } return list.toArray(new String[] {}); } private void genRejectReportRecord(final PrintWriter pw, final int rCount, final int lCount, final String field, final String eMsg, final Throwable cause) { pw.println(SqltoolRB.rejectreport_row.getString( "sqltool-" + ((rCount % 2 == 0) ? "even" : "odd"), Integer.toString(rCount), Integer.toString(lCount), ((field == null) ? "" : field), (((eMsg == null) ? "" : eMsg) + ((eMsg == null || cause == null) ? "" : "
") + ((cause == null) ? "" : ( (cause instanceof SQLException && cause.getMessage() != null) ? cause.getMessage() : cause.toString() ) ) ))); } /** * Parses input into command tokens, but does not perform the commands * (unless you consider parsing blocks of nested commands to be "performing" * a command). * *

Throws only if I/O error or if nestingCommand != null and EOF * encountered before end of entire file (encountered at any level of * recursion). * *

Exceptions thrown within this method percolate right up to the * external call (in scanpass), regardless of ContinueOnErr setting. This is * because it's impossible to know when to terminate blocks if there is a * parsing error. * *

Only a separate SqlFile invocation (incl. \i command) will cause a * seekTokenSource exception to be handled at a level other than the very * top. * * @param nestingCommand Set to null to read scanner until EOF. * @throws BadSpecial * @throws IOException * @throws SqlToolError * @return TokenList */ private TokenList seekTokenSource(final String nestingCommand) throws BadSpecial, IOException, SqlToolError { Token token; TokenList newTS = new TokenList(); Pattern endPattern = null; Pattern elsePattern = null; if (nestingCommand != null) if (nestingCommand.equals("if")) { endPattern = Pattern.compile("end\\s+" + nestingCommand); elsePattern = Pattern.compile("else"); } else if (nestingCommand.equals("else")) { endPattern = Pattern.compile("end\\s+if"); } else { endPattern = Pattern.compile("end\\s+" + nestingCommand); } String subNestingCommand; Matcher inlineNestMatcher; while ((token = scanner.yylex()) != null) { if (endPattern != null && token.type == Token.PL_TYPE && endPattern.matcher(token.val).matches()) return newTS; if (elsePattern != null && token.type == Token.PL_TYPE && elsePattern.matcher(token.val).matches()) { assert token.nestedBlock == null: "else statement's .nested block not null"; token.nestedBlock = seekTokenSource("else"); newTS.add(token); return newTS; } inlineNestMatcher = inlineNestMatcher(token); if (inlineNestMatcher != null) { processInlineBlock(token, inlineNestMatcher.group(1), inlineNestMatcher.group(2)); } else { subNestingCommand = nestingCommand(token); if (subNestingCommand != null) token.nestedBlock = seekTokenSource(subNestingCommand); } newTS.add(token); } if (nestingCommand == null) return newTS; throw new BadSpecial( SqltoolRB.pl_block_unterminated.getString(nestingCommand)); } /** * We want leading space to be trimmed. Leading space should probably not be * trimmed, but it is trimmed now (by the Scanner). * * @param defToken Token * @throws BadSpecial */ private void processMacro(final Token defToken) throws BadSpecial { Matcher matcher; Token macroToken; if (defToken.val.length() < 1) throw new BadSpecial(SqltoolRB.macro_tip.getString()); int newType = -1; StringBuffer newVal = new StringBuffer(); switch (defToken.val.charAt(0)) { case '?': stdprintln(SqltoolRB.macro_help.getString()); break; case ':': matcher = editMacroPattern.matcher(defToken.val); if (!matcher.matches()) throw new BadSpecial(SqltoolRB.macro_malformat.getString()); if (buffer == null) { stdprintln(nobufferYetString); return; } newVal.append(buffer.val); if (matcher.groupCount() > 1 && matcher.group(2) != null && matcher.group(2).length() > 0) newVal.append(matcher.group(2)); newType = buffer.type; if (newVal.length() < 1) throw new BadSpecial(SqltoolRB.macrodef_empty.getString()); if (newVal.charAt(newVal.length() - 1) == ';') throw new BadSpecial(SqltoolRB.macrodef_semi.getString()); shared.macros.put(matcher.group(1), new Token(buffer.type, newVal, defToken.line)); break; case '=': String defString = defToken.val; defString = defString.substring(1).trim(); if (defString.length() < 1) { for (Map.Entry entry : shared.macros.entrySet()) stdprintln(entry.getKey() + " = " + entry.getValue().reconstitute()); break; } matcher = legacyEditMacroPattern.matcher(defString); if (matcher.matches()) { if (buffer == null) { stdprintln(nobufferYetString); return; } newVal.append(buffer.val); if (matcher.groupCount() > 1 && matcher.group(2) != null && matcher.group(2).length() > 0) newVal.append(matcher.group(2)); newType = buffer.type; } else { matcher = spMacroPattern.matcher(defString); if (matcher.matches()) { newVal.append(matcher.group(3)); newType = (matcher.group(2).equals("*") ? Token.PL_TYPE : Token.SPECIAL_TYPE); } else { matcher = sqlMacroPattern.matcher(defString); if (!matcher.matches()) throw new BadSpecial( SqltoolRB.macro_malformat.getString()); newVal.append(matcher.group(2)); newType = Token.SQL_TYPE; } } if (newVal.length() < 1) throw new BadSpecial(SqltoolRB.macrodef_empty.getString()); if (newVal.charAt(newVal.length() - 1) == ';') throw new BadSpecial(SqltoolRB.macrodef_semi.getString()); shared.macros.put(matcher.group(1), new Token(newType, newVal, defToken.line)); break; default: matcher = useFnPattern.matcher(defToken.val); if (matcher.matches()) { macroToken = shared.macros.get(matcher.group(1) + ')'); if (macroToken == null) throw new BadSpecial( SqltoolRB.macro_undefined.getString( matcher.group(1) + "...)")); String[] splitVars = null; if (matcher.groupCount() > 1 && matcher.group(2) != null && matcher.group(2).length() > 0) { //buffer.val += matcher.group(2); splitVars = matcher.group(2).split("\\s*,\\s*", -1); } else { splitVars = new String[0]; } String thirdGroup = (matcher.groupCount() > 2 && matcher.group(3) != null) ? matcher.group(3) : null; preempt = thirdGroup != null && thirdGroup.endsWith(";"); if (preempt) { if (thirdGroup.length() == 1) { thirdGroup = null; } else { thirdGroup = thirdGroup.substring(0, thirdGroup.length() - 1); } } Matcher templateM = fnParamPat.matcher(macroToken.val); int prevEnd = 0; String varVal; int varNum; setBuf(macroToken); buffer.val = ""; buffer.line = defToken.line; while (templateM.find()) { buffer.val += macroToken.val .substring(prevEnd, templateM.start()); varNum = Integer.parseInt( templateM.group(templateM.groupCount())); varVal = (varNum > 0 && varNum <= splitVars.length) ? splitVars[varNum-1] : null; if (varVal == null && (templateM.groupCount() < 2 || templateM.group(1) == null || templateM.group(1).length() < 1)) throw new BadSpecial( SqltoolRB.plvar_undefined.getString( templateM.group(templateM.groupCount()))); if (varVal != null) buffer.val += varVal; prevEnd = templateM.end(); } buffer.val += macroToken.val.substring(prevEnd); if (thirdGroup != null) buffer.val += thirdGroup; return; } matcher = useMacroPattern.matcher(defToken.val); if (!matcher.matches()) throw new BadSpecial(SqltoolRB.macro_malformat.getString()); macroToken = shared.macros.get(matcher.group(1)); if (macroToken == null) throw new BadSpecial(SqltoolRB.macro_undefined.getString( matcher.group(1))); setBuf(macroToken); buffer.line = defToken.line; if (matcher.groupCount() > 1 && matcher.group(2) != null && matcher.group(2).length() > 0) buffer.val += matcher.group(2); preempt = matcher.group(matcher.groupCount()).equals(";"); } } /** * Convert a String to a byte array by interpreting every 2 characters as an * octal byte value. * * @param hexChars String * @return byte[] */ public static byte[] hexCharOctetsToBytes(final String hexChars) { int chars = hexChars.length(); if (chars != (chars / 2) * 2) throw new NumberFormatException("Hex character lists contains " + "an odd number of characters: " + chars); byte[] ba = new byte[chars/2]; int offset = 0; char c; int octet; for (int i = 0; i < chars; i++) { octet = 0; c = hexChars.charAt(i); if (c >= 'a' && c <= 'f') { octet += 10 + c - 'a'; } else if (c >= 'A' && c <= 'F') { octet += 10 + c - 'A'; } else if (c >= '0' && c <= '9') { octet += c - '0'; } else { throw new NumberFormatException( "Non-hex character in input at offset " + i + ": " + c); } octet = octet << 4; c = hexChars.charAt(++i); if (c >= 'a' && c <= 'f') { octet += 10 + c - 'a'; } else if (c >= 'A' && c <= 'F') { octet += 10 + c - 'A'; } else if (c >= '0' && c <= '9') { octet += c - '0'; } else { throw new NumberFormatException( "Non-hex character in input at offset " + i + ": " + c); } ba[offset++] = (byte) octet; } assert ba.length == offset: "Internal accounting problem. Expected to fill buffer of " + "size "+ ba.length + ", but wrote only " + offset + " bytes"; return ba; } /** * Just a stub for now. * * @param hexChars String * @return byte[] */ public static byte[] bitCharsToBytes(final String hexChars) { if (hexChars == null) throw new NullPointerException(); // To shut up compiler warn throw new NumberFormatException( "Sorry. Bit exporting not supported yet"); } private void requireConnection() throws SqlToolError { if (shared.jdbcConn == null) throw new SqlToolError(SqltoolRB.no_required_conn.getString()); } /** * Returns a String report for the specified JDBC Connection. For databases * with poor JDBC support, you won't get much detail. * * @param c Connection * @return String */ public static String getBanner(final Connection c) { try { DatabaseMetaData md = c.getMetaData(); return (md == null) ? null : SqltoolRB.jdbc_established.getString( md.getDatabaseProductName(), md.getDatabaseProductVersion(), md.getUserName(), (c.isReadOnly() ? "R/O " : "R/W ") + RCData.tiToString( c.getTransactionIsolation())); } catch (SQLException se) { return null; } } private void displayConnBanner() { String msg = (shared.jdbcConn == null) ? SqltoolRB.disconnected_msg.getString() : SqlFile.getBanner(shared.jdbcConn); stdprintln((msg == null) ? SqltoolRB.connected_fallbackmsg.getString() : msg); } private URL dereferenceAtToUrl(final String s) throws BadSpecial { try { if (s.indexOf('@') != 0) return SqlFile.URL_WITH_PROTO_RE.matcher(s).matches() ? new URL(s) : new URL("file", null, s); if (atBase == null) throw new BadSpecial(SqltoolRB.illegal_at.getString()); return (s.length() > 2 && (s.charAt(1) == '/' || s.charAt(1) == '\\')) ? new URL(atBase, s.substring(2)) : new URL(atBase.getProtocol(), atBase.getHost(), atBase.getFile() + s.substring(1)); } catch (MalformedURLException mue) { throw new BadSpecial(mue.getMessage()); } } private File dereferenceAtToFile(final String s) throws BadSpecial { if (s.indexOf('@') != 0) return new File(s); if (atBase == null) throw new BadSpecial(SqltoolRB.illegal_at.getString()); if (atBase.getProtocol() != "file") throw new BadSpecial(SqltoolRB.desturl_nowrite.getString( atBase.toString())); try { return new File(((s.length() > 2 && (s.charAt(1) == '/' || s.charAt(1) == '\\')) ? new URL(atBase, s.substring(2)) : new URL(atBase.getProtocol(), atBase.getHost(), atBase.getFile() + s.substring(1))).getPath()); } catch (MalformedURLException mue) { throw new BadSpecial(mue.getMessage()); } } /** * Escaping rules taken from 'Reserved Characters in HTML table at * http://www.w3schools.com/tags/ref_entities.asp * * @param s String * @return String */ public static String escapeHtml(final String s) { StringBuilder sb = new StringBuilder(); char[] charArray = s.toCharArray(); for (char c : charArray) switch (c) { case '"': sb.append("""); break; case '\'': // This case not supported by Commons Lang's escapeHtml sb.append("'"); break; case '&': sb.append("&"); break; case '<': sb.append("<"); break; case '>': sb.append(">"); break; default: sb.append(c); } return sb.toString(); } /** * * @param pWriter PrintWriter * @param filePath only used for error reporting. * @throws BadSpecial * @throws SqlToolError */ private void writeHeader(final PrintWriter pWriter, final String filePath) throws BadSpecial, SqlToolError { char[] readBfr = new char[1024]; int i; StringWriter sWriter = new StringWriter(); InputStreamReader isr = null; String str; try { InputStream is = (topHtmlFile == null) ? getClass().getResourceAsStream( "sqltool/top-boilerplate.html") : new FileInputStream(topHtmlFile); if (is == null) throw new IOException("Missing resource: " + ((topHtmlFile == null) ? topHtmlFile : "sqltool/top-boilerplate")); isr = new InputStreamReader(is); while ((i = isr.read(readBfr)) > -1) sWriter.write(readBfr, 0, i); readBfr = null; str = sWriter.toString(); sWriter.close(); } catch (Exception e) { throw new BadSpecial( SqltoolRB.file_writefail.getString(filePath), e); } finally { try { if (isr != null) isr.close(); } catch (IOException ioe) { // TODO: Throw appropriate exception } } pWriter.write(dereference(str.replaceAll("\\r?\\n", LS), true)); } /** * @param pwQuery the writer * @param filePath only used for error reporting. * @throws SqlToolError */ private void writeFooter(final PrintWriter pwQuery, final String filePath) throws SqlToolError { char[] readBfr = new char[1024]; int i; StringWriter sWriter = new StringWriter(); InputStreamReader isr = null; String str; try { InputStream is = (bottomHtmlFile == null) ? getClass().getResourceAsStream( "sqltool/bottom-boilerplate.html") : new FileInputStream(bottomHtmlFile); if (is == null) throw new IOException("Missing resource: " + ((bottomHtmlFile == null) ? bottomHtmlFile : "sqltool/bottom-boilerplate")); isr = new InputStreamReader(is); while ((i = isr.read(readBfr)) > -1) sWriter.write(readBfr, 0, i); readBfr = null; str = sWriter.toString(); sWriter.close(); } catch (Exception e) { throw new SqlToolError( SqltoolRB.file_writefail.getString(filePath), e); } finally { try { if (isr != null) isr.close(); } catch (IOException ioe) { // TODO: Throw appropriate exception } } pwQuery.write(dereference(str.replaceAll("\\r?\\n", LS), true)); } private void processInlineBlock(final Token t, final String ifCmdText, final String nestingText) throws BadSpecial, IOException, SqlToolError { assert t.nestedBlock == null: "Inline-nest command has .nestBlock pre-populated"; SqlFileScanner storedScanner = scanner; try { scanner = new SqlFileScanner(new StringReader(nestingText + '\n')); scanner.setStdPrintStream(shared.psStd); scanner.setRawLeadinPrompt(""); scanner.setInteractive(interactive); t.nestedBlock = seekTokenSource(null); } finally { scanner = storedScanner; } t.val = ifCmdText; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy