/* Copyright (c) 2001-2010, The HSQL Development Group * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the HSQL Development Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.hsqldb.cmdline; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.HashSet; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.logging.Level; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; import org.hsqldb.lib.AppendableException; import org.hsqldb.lib.RCData; import org.hsqldb.lib.StringUtil; import org.hsqldb.lib.FrameworkLogger; import org.hsqldb.cmdline.sqltool.Token; import org.hsqldb.cmdline.sqltool.TokenList; import org.hsqldb.cmdline.sqltool.TokenSource; import org.hsqldb.cmdline.sqltool.SqlFileScanner; /* $Id: SqlFile.java 3628 2010-06-06 00:44:08Z unsaved $ */ /** * Encapsulation of SQL text and the environment under which it will executed * with a JDBC Connection. * 'SqlInputStream' would be a more precise name, but the content we are * talking about here is what is colloqially known as the contents of * "SQL file"s. * <P> * The file <CODE>src/org/hsqldb/sample/SqlFileEmbedder.java</CODE> * in the HSQLDB distribution provides an example for using SqlFile to * execute SQL files directly from your own Java classes. * <P/><P> * The complexities of passing userVars and macros maps are to facilitate * strong scoping (among blocks and nested scripts). * <P/><P> * Some implementation comments and variable names use keywords based * on the following definitions. <UL> * <LI> COMMAND = Statement || SpecialCommand || BufferCommand * <LI>Statement = SQL statement like "SQL Statement;" * <LI>SpecialCommand = Special Command like "\x arg..." * <LI>BufferCommand = Editing/buffer command like ":s/this/that/" * </UL> * <P/><P> * When entering SQL statements, you are always "appending" to the * "immediate" command (not the "buffer", which is a different thing). * All you can do to the immediate command is append new lines to it, * execute it, or save it to buffer. * When you are entering a buffer edit command like ":s/this/that/", * your immediate command is the buffer-edit-command. The buffer * is the command string that you are editing. * The buffer usually contains either an exact copy of the last command * executed or sent to buffer by entering a blank line, * but BUFFER commands can change the contents of the buffer. * <P/><P> * In general, the special commands mirror those of Postgresql's psql, * but SqlFile handles command editing very differently than Postgresql * does, in part because of Java's lack of support for raw tty I/O. * The \p special command, in particular, is very different from psql's. * <P/><P> * Buffer commands are unique to SQLFile. The ":" commands allow * you to edit the buffer and to execute the buffer. * <P/><P> * \d commands are very poorly supported for Mysql because * (a) Mysql lacks most of the most basic JDBC support elements, and * the most basic role and schema features, and * (b) to access the Mysql data dictionary, one must change the database * instance (to do that would require work to restore the original state * and could have disastrous effects upon transactions). * <P/><P> * The process*() methods, other than processBuffHist() ALWAYS execute * on "buffer", and expect it to contain the method specific prefix * (if any). * <P/><P> * The input/output Reader/Stream are generally managed by the caller. * An exception is that the input reader may be closed automatically or on * demand by the user, since in some cases this class builds the Reader. * There is no corresponding functionality for output since the user always * has control over that object (which may be null or System.out). * <P/> * * @see <a href="../../../../util-guide/sqltool-chapt.html" target="guide"> * The SqlTool chapter of the * HyperSQL Utilities Guide</a> * @see org.hsqldb.sample.SqlFileEmbedder * @version $Revision: 3628 $, $Date: 2010-06-05 20:44:08 -0400 (Sat, 05 Jun 2010) $ * @author Blaine Simpson (blaine dot simpson at admc dot com) */ public class SqlFile { private static FrameworkLogger logger = FrameworkLogger.getLog(SqlFile.class); private static final int DEFAULT_HISTORY_SIZE = 40; private boolean executing; private boolean permitEmptySqlStatements; private boolean interactive; private String primaryPrompt = "sql> "; private static String rawPrompt; private static Method createArrayOfMethod; private String contPrompt = " +> "; private boolean htmlMode; private TokenList history; private String nullRepToken; private String dsvTargetFile; private String dsvTargetTable; private String dsvConstCols; private String dsvRejectFile; private String dsvRejectReport; private int dsvRecordsPerCommit = 0; /** Platform-specific line separator */ public static String LS = System.getProperty("line.separator"); private int maxHistoryLength = 1; // TODO: Implement PL variable to interactively change history length. // Study to be sure this won't cause state inconsistencies. private boolean reportTimes; private Reader reader; // Reader serves the auxiliary purpose of null meaning execute() // has finished. private String inputStreamLabel; static String DEFAULT_FILE_ENCODING = System.getProperty("file.encoding"); /** * N.b. javax.util.regex Optional capture groups (...)? are completely * unpredictable wrt whether you get a null capture group vs. no capture. * Must always check count! */ private static Pattern specialPattern = Pattern.compile("(\\S+)(?:\\s+(.*\\S))?\\s*"); private static Pattern plPattern = Pattern.compile("(.*\\S)?\\s*"); private static Pattern foreachPattern = Pattern.compile("foreach\\s+(\\S+)\\s*\\(([^)]+)\\)\\s*"); private static Pattern ifwhilePattern = Pattern.compile("\\S+\\s*\\(([^)]*)\\)\\s*"); private static Pattern varsetPattern = Pattern.compile("(\\S+)\\s*([=_~])\\s*(?:(.*\\S)\\s*)?"); private static Pattern substitutionPattern = Pattern.compile("(\\S)(.+?)\\1(.*?)\\1(.+)?\\s*"); // Note that this pattern does not include the leading ":s". private static Pattern slashHistoryPattern = Pattern.compile("\\s*/([^/]+)/\\s*(\\S.*)?"); private static Pattern historyPattern = Pattern.compile("\\s*(-?\\d+)?\\s*(\\S.*)?"); // Note that this pattern does not include the leading ":". private static Pattern wincmdPattern; private static Pattern useMacroPattern = Pattern.compile("(\\w+)(\\s.*[^;])?(;?)"); private static Pattern editMacroPattern = Pattern.compile("(\\w+)\\s*:(.*)"); private static Pattern spMacroPattern = Pattern.compile("(\\w+)\\s+([*\\\\])(.*\\S)"); private static Pattern sqlMacroPattern = Pattern.compile("(\\w+)\\s+(.*\\S)"); private static Pattern integerPattern = Pattern.compile("\\d+"); private static Pattern nameValPairPattern = Pattern.compile("\\s*(\\w+)\\s*=(.*)"); // Specifically permits 0-length values, but not names. private static Pattern dotPattern = Pattern.compile("(\\w*)\\.(\\w*)"); private static Pattern commitOccursPattern = Pattern.compile("(?is)(?:set\\s+autocommit.*)|(commit\\s*)"); private static Pattern logPattern = Pattern.compile("(?i)(FINER|WARNING|SEVERE|INFO|FINEST)\\s+(.*\\S)"); private static Pattern arrayPattern = Pattern.compile("ARRAY\\s*\\[\\s*(.*\\S)?\\s*\\]"); private static Map<String, Pattern> nestingPLCommands = new HashMap<String, Pattern>(); static { nestingPLCommands.put("if", ifwhilePattern); nestingPLCommands.put("while", ifwhilePattern); nestingPLCommands.put("foreach", foreachPattern); if (System.getProperty("os.name").startsWith("Windows")) { wincmdPattern = Pattern.compile("([^\"]+)?(\"[^\"]*\")?"); } rawPrompt = SqltoolRB.rawmode_prompt.getString() + "> "; DSV_OPTIONS_TEXT = SqltoolRB.dsv_options.getString(); D_OPTIONS_TEXT = SqltoolRB.d_options.getString(); DSV_X_SYNTAX_MSG = SqltoolRB.dsv_x_syntax.getString(); DSV_M_SYNTAX_MSG = SqltoolRB.dsv_m_syntax.getString(); nobufferYetString = SqltoolRB.nobuffer_yet.getString(); try { SqlFile.createArrayOfMethod = Connection.class.getDeclaredMethod( "createArrayOf", String.class, Object[].class); } catch (Exception expectedException) { // Purposeful no-op. Leave createArrayOfMethod null. } } // This can throw a runtime exception, but since the pattern // Strings are constant, one test run of the program will tell // if the patterns are good. /** * Encapsulate updating local variables which depend upon PL variables. * <P> * Right now this is called whenever the user variable map is changed. * It would be more efficient to do it JIT by keeping track of when * the vars may be "dirty" by a variable map change, and having all * methods that use the settings call a conditional updater, but that * is less reliable since there is no way to guarantee that the vars * are not used without checking. * UPDATE: Could do what is needed by making a Map subclass with * overridden setters which enforce dirtiness. * <P/> */ private void updateUserSettings() { dsvSkipPrefix = SqlFile.convertEscapes( shared.userVars.get("*DSV_SKIP_PREFIX")); if (dsvSkipPrefix == null) { dsvSkipPrefix = DEFAULT_SKIP_PREFIX; } dsvSkipCols = shared.userVars.get("*DSV_SKIP_COLS"); dsvTrimAll = Boolean.parseBoolean( shared.userVars.get("*DSV_TRIM_ALL")); dsvColDelim = SqlFile.convertEscapes( shared.userVars.get("*DSV_COL_DELIM")); if (dsvColDelim == null) { dsvColDelim = SqlFile.convertEscapes( shared.userVars.get("*CSV_COL_DELIM")); } if (dsvColDelim == null) { dsvColDelim = DEFAULT_COL_DELIM; } dsvColSplitter = shared.userVars.get("*DSV_COL_SPLITTER"); if (dsvColSplitter == null) { dsvColSplitter = DEFAULT_COL_SPLITTER; } dsvRowDelim = SqlFile.convertEscapes( shared.userVars.get("*DSV_ROW_DELIM")); if (dsvRowDelim == null) { dsvRowDelim = SqlFile.convertEscapes( shared.userVars.get("*CSV_ROW_DELIM")); } if (dsvRowDelim == null) { dsvRowDelim = DEFAULT_ROW_DELIM; } dsvRowSplitter = shared.userVars.get("*DSV_ROW_SPLITTER"); if (dsvRowSplitter == null) { dsvRowSplitter = DEFAULT_ROW_SPLITTER; } dsvTargetFile = shared.userVars.get("*DSV_TARGET_FILE"); if (dsvTargetFile == null) { dsvTargetFile = shared.userVars.get("*CSV_FILEPATH"); } dsvTargetTable = shared.userVars.get("*DSV_TARGET_TABLE"); if (dsvTargetTable == null) { dsvTargetTable = shared.userVars.get("*CSV_TABLENAME"); // This just for legacy variable name. } dsvConstCols = shared.userVars.get("*DSV_CONST_COLS"); dsvRejectFile = shared.userVars.get("*DSV_REJECT_FILE"); dsvRejectReport = shared.userVars.get("*DSV_REJECT_REPORT"); if (shared.userVars.get("*DSV_RECORDS_PER_COMMIT") != null) try { dsvRecordsPerCommit = Integer.parseInt( shared.userVars.get("*DSV_RECORDS_PER_COMMIT")); } catch (NumberFormatException nfe) { logger.error(SqltoolRB.reject_rpc.getString( shared.userVars.get("*DSV_RECORDS_PER_COMMIT"))); shared.userVars.remove("*DSV_REJECT_REPORT"); dsvRecordsPerCommit = 0; } nullRepToken = shared.userVars.get("*NULL_REP_TOKEN"); if (nullRepToken == null) { nullRepToken = shared.userVars.get("*CSV_NULL_REP"); } if (nullRepToken == null) { nullRepToken = DEFAULT_NULL_REP; } } /** * Private class to "share" attributes among a family of SqlFile instances. */ private static class SharedFields { /* Since SqlTool can run against different versions of HSQLDB (plus * against any JDBC database), it can't make assumptions about * commands which may cause implicit commits, or commit state * requirements with specific databases may have for specific SQL * statements. Therefore, we just assume that any statement other * than COMMIT or SET AUTOCOMMIT causes an implicit COMMIT (the * Java API spec mandates that setting AUTOCOMMIT causes an implicit * COMMIT, regardless of whether turning AUTOCOMMIT on or off). */ boolean possiblyUncommitteds; Connection jdbcConn; Map<String, String> userVars = new HashMap<String, String>(); Map<String, Token> macros = new HashMap<String, Token>(); PrintStream psStd; SharedFields(PrintStream psStd) { this.psStd = psStd; } String encoding; } private SharedFields shared; private static final String DIVIDER = "-----------------------------------------------------------------" + "-----------------------------------------------------------------"; // Needs to be at least as wide as the widest field or header displayed. private static String revnum = "$Revision: 3628 $".substring("$Revision: ".length(), "$Revision: 3628 $".length() - 2); private static String DSV_OPTIONS_TEXT; private static String D_OPTIONS_TEXT; /** * Convenience wrapper for the SqlFile(File, String) constructor * * @throws IOException * @see #SqlFile(File, String) */ public SqlFile(File inputFile) throws IOException { this(inputFile, null); } /** * Convenience wrapper for the SqlFile(File, String, boolean) constructor * * @param encoding is applied to both the given File and other files * read in or written out. Null will use your env+JVM settings. * @throws IOException * @see #SqlFile(File, String, boolean) */ public SqlFile(File inputFile, String encoding) throws IOException { this(inputFile, encoding, false); } /** * Constructor for non-interactive usage with a SQL file, using the * specified encoding and sending normal output to stdout. * * @param encoding is applied to the given File and other files * read in or written out. Null will use your env+JVM settings. * @param interactive If true, prompts are printed, the interactive * Special commands are enabled, and * continueOnError defaults to true. * @throws IOException * @see #SqlFile(Reader, String, PrintStream, String, boolean) */ public SqlFile(File inputFile, String encoding, boolean interactive) throws IOException { this(new InputStreamReader(new FileInputStream(inputFile), (encoding == null) ? DEFAULT_FILE_ENCODING : encoding), inputFile.toString(), System.out, encoding, interactive); } /** * Constructor for interactive usage with stdin/stdout * * @param encoding is applied to other files read in or written out (but * not to stdin or stdout). * Null will use your env+JVM settings. * @param interactive If true, prompts are printed, the interactive * Special commands are enabled, and * continueOnError defaults to true. * @throws IOException * @see #SqlFile(Reader, String, PrintStream, String, boolean) */ public SqlFile(String encoding, boolean interactive) throws IOException { this((encoding == null) ? new InputStreamReader(System.in) : new InputStreamReader(System.in, encoding), "<stdin>", System.out, encoding, interactive); } /** * Instantiate a SqlFile instance for SQL input from 'reader'. * * After any needed customization, the SQL can be executed by the * execute method. * <P> * Most Special Commands and many Buffer commands are only for * interactive use. * </P> <P> * This program never writes to an error stream (stderr or alternative). * All meta messages and error messages are written using the logging * facility. * </P> * * @param reader Source for the SQL to be executed. * Caller is responsible for setting up encoding. * (the 'encoding' parameter will NOT be applied * to this reader). * @param psStd PrintStream for normal output. * If null, normal output will be discarded. * Caller is responsible for settingup encoding * (the 'encoding' parameter will NOT be applied * to this stream). * @param interactive If true, prompts are printed, the interactive * Special commands are enabled, and * continueOnError defaults to true. * @throws IOException * @see #execute() */ public SqlFile(Reader reader, String inputStreamLabel, PrintStream psStd, String encoding, boolean interactive) throws IOException { this(reader, inputStreamLabel); try { shared = new SharedFields(psStd); setEncoding(encoding); this.interactive = interactive; continueOnError = this.interactive; if (interactive) { history = new TokenList(); maxHistoryLength = DEFAULT_HISTORY_SIZE; } updateUserSettings(); // Updates local vars basd on * shared.userVars // even when (like now) these are all defaults. } catch (IOException ioe) { closeReader(); throw ioe; } catch (RuntimeException re) { closeReader(); throw re; } } /** * Wrapper for SqlFile(SqlFile, Reader, String) * * @see #SqlFile(SqlFile, Reader, String) */ private SqlFile(SqlFile parentSqlFile, File inputFile) throws IOException { this(parentSqlFile, new InputStreamReader(new FileInputStream(inputFile), (parentSqlFile.shared.encoding == null) ? DEFAULT_FILE_ENCODING : parentSqlFile.shared.encoding), inputFile.toString()); } /** * Constructor for recursion */ private SqlFile(SqlFile parentSqlFile, Reader reader, String inputStreamLabel) { this(reader, inputStreamLabel); try { recursed = true; shared = parentSqlFile.shared; plMode = parentSqlFile.plMode; interactive = false; continueOnError = parentSqlFile.continueOnError; // Nested input is non-interactive because it just can't work to // have user append to edit buffer, and displaying prompts would // be misleading and inappropriate; yet we will inherit the current // continueOnError behavior. updateUserSettings(); // Updates local vars basd on * shared.userVars } catch (RuntimeException re) { closeReader(); throw re; } } /** * Base Constructor which every other Constructor starts with */ private SqlFile(Reader reader, String inputStreamLabel) { logger.privlog(Level.FINER, "<init>ting SqlFile instance", null, 2, FrameworkLogger.class); if (reader == null) throw new IllegalArgumentException("'reader' may not be null"); if (inputStreamLabel == null) throw new IllegalArgumentException( "'inputStreamLabel' may not be null"); // Don't try to verify reader.ready() here, since we require it to be // reayd to read only in execute(), plus in many caess it's useful for // execute() to block. this.reader = reader; this.inputStreamLabel = inputStreamLabel; } public void setConnection(Connection jdbcConn) { if (jdbcConn == null) throw new IllegalArgumentException( "We don't yet support unsetting the JDBC Connection"); shared.jdbcConn = jdbcConn; } public Connection getConnection() { return shared.jdbcConn; } public void setContinueOnError(boolean continueOnError) { this.continueOnError = continueOnError; } public void setMaxHistoryLength(int maxHistoryLength) { if (executing) throw new IllegalStateException( "Can't set maxHistoryLength after execute() has been called"); if (reader == null) throw new IllegalStateException( "Can't set maxHistoryLength execute() has run"); this.maxHistoryLength = maxHistoryLength; } public void addMacros(Map<String, Token> newMacros) { shared.macros.putAll(newMacros); } public void addUserVars(Map<String, String> newUserVars) { shared.userVars.putAll(newUserVars); } public Map<String, String> getUserVars() { // Consider whether safer to return a deep copy. Probably. return shared.userVars; } public Map<String, Token> getMacros() { // Consider whether safer to return a deep copy. Probably. return shared.macros; } /** * This sets the instance variable and the corresponding PL variable. * * @param newEncoding may be null to revert to using defaults again. */ private void setEncoding(String newEncoding) throws UnsupportedEncodingException { if (newEncoding == null) { shared.encoding = null; shared.userVars.remove("ENCODING"); return; } if (!Charset.isSupported(newEncoding)) throw new UnsupportedEncodingException(newEncoding); shared.userVars.put("*ENCODING", newEncoding); shared.encoding = newEncoding; } // So we can tell how to handle quit and break commands. private boolean recursed; private PrintWriter pwQuery; private PrintWriter pwDsv; private boolean continueOnError; /* * This is reset upon each execute() invocation (to true if interactive, * false otherwise). */ private SqlFileScanner scanner; private Token buffer; private boolean preempt; private String lastSqlStatement; private boolean autoClose = true; /** * Specify whether the supplied or generated input Reader should * automatically be closed by the execute() method. * <P> * execute() will close the Reader by default (i.e. 'autoClose' defaults * to true). * You may want to set this to false if you want to stop execution with * \q or similar, then continue using the Reader or underlying Stream. * </P> <P> * The caller is always responsible for closing the output object (if any) * used by SqlFile. * </P> */ public void setAutoClose(boolean autoClose) { this.autoClose = autoClose; } /** * Process all the commands from the file or Reader associated with * "this" object. * SQL commands in the content get executed against the current JDBC * data source connection. * * @throws SQLExceptions thrown by JDBC driver. * Only possible if in "\c false" mode. * @throws SqlToolError all other errors. * This includes including QuitNow, BreakException, * ContinueException for recursive calls only. */ synchronized public void execute() throws SqlToolError, SQLException { if (reader == null) throw new IllegalStateException("Can't call execute() " + "more than once for a single SqlFile instance"); try { scanner = new SqlFileScanner(reader); scanner.setStdPrintStream(shared.psStd); scanner.setRawLeadinPrompt(SqltoolRB.raw_leadin.getString()); if (interactive) { stdprintln(SqltoolRB.SqlFile_banner.getString(revnum)); scanner.setRawPrompt(rawPrompt); scanner.setSqlPrompt(contPrompt); scanner.setSqltoolPrompt(primaryPrompt); scanner.setInteractive(true); if (shared.jdbcConn == null) stdprintln("To connect to a data source, use '\\j " + "urlid' or '\\j account password jdbc:url...'"); stdprint(primaryPrompt); } scanpass(scanner); } finally { try { closeQueryOutputStream(); if (autoClose) closeReader(); } finally { reader = null; // Encourage GC of buffers } } } /** * Close the reader. * * The execute method will run this automatically, by default. */ public void closeReader() { if (reader == null) { return; } try { if (scanner != null) try { scanner.yyclose(); } catch (IOException ioe) { errprintln("Failed to close pipes"); } try { reader.close(); } catch (IOException ioe) { // Purposefully empty. // The reader will usually already be closed at this point. } } finally { reader = null; // Encourage GC of buffers } } /** * Returns normalized nesting command String, like "if" or "foreach". * If command is not a nesting command, returns null; * If there's a proper command String, but the entire PL command is * malformatted, throws. */ private String nestingCommand(Token token) throws BadSpecial { if (token.type != Token.PL_TYPE) return null; // The scanner assures that val is non-null for PL_TYPEs. String commandWord = token.val.replaceFirst("\\s.*", ""); if (!nestingPLCommands.containsKey(commandWord)) return null; Pattern pattern = nestingPLCommands.get(commandWord); if (pattern.matcher(token.val).matches()) return commandWord; throw new BadSpecial(SqltoolRB.pl_malformat.getString()); } synchronized protected void scanpass(TokenSource ts) throws SqlToolError, SQLException { boolean rollbackUncoms = true; String nestingCommand; Token token = null; if (shared.userVars.size() > 0) { plMode = true; } try { while (true) try { if (preempt) { token = buffer; preempt = false; } else { token = ts.yylex(); logger.finest("SqlFile got new token: " + token); } if (token == null) break; nestingCommand = nestingCommand(token); if (nestingCommand != null) { if (token.nestedBlock == null) { token.nestedBlock = seekTokenSource(nestingCommand); /* This command (and the same recursive call inside * of the seekTokenSource() method) ensure that all * "blocks" are tokenized immediately as block * commands are encountered, and the blocks are * tokenized in their entirety all the way to the * leaves. */ } processBlock(token); /* processBlock recurses through scanpass(), * which processes the nested commands which have * (in all cases) already beeen tokenized. */ continue; } switch (token.type) { case Token.SYNTAX_ERR_TYPE: throw new SqlToolError(SqltoolRB.input_malformat.getString()); // Will get here if Scanner can't match input to any // known command type. // An easy way to get here is to start a command with // quotes. case Token.UNTERM_TYPE: throw new SqlToolError( SqltoolRB.input_unterminated.getString( token.val)); case Token.RAW_TYPE: case Token.RAWEXEC_TYPE: /* * A real problem in this block is that the Scanner * has already displayed the next prompt at this * point. We handle this specially within this * block, but if we throw, the handler will not * know that the prompt has to be re-displayed. * I.e., KNOWN ISSUE: For some errors caught during * raw command execution, interactive users will not * get a prompt to tell them to proceed. */ if (token.val == null) token.val = ""; /* * Don't have time know to figure out whether it would * ever be useful to send just (non-zero) whitespace * to the DB. Prohibiting for now. */ if (token.val.trim().length() < 1) { throw new SqlToolError( SqltoolRB.raw_empty.getString()); } int receivedType = token.type; token.type = Token.SQL_TYPE; if (setBuf(token) && receivedType == Token.RAW_TYPE && interactive) { stdprintln(""); stdprintln(SqltoolRB.raw_movedtobuffer.getString()); stdprint(primaryPrompt); // All of these stdprint*'s are to work around a // very complicated issue where the Scanner // has already displayed the next prompt before // we can display our status message. } if (receivedType == Token.RAWEXEC_TYPE) { historize(); processSQL(); } continue; case Token.MACRO_TYPE: processMacro(token); continue; case Token.PL_TYPE: setBuf(token); historize(); processPL(null); continue; case Token.SPECIAL_TYPE: setBuf(token); historize(); processSpecial(null); continue; case Token.EDIT_TYPE: // Scanner only returns EDIT_TYPEs in interactive mode processBuffHist(token); continue; case Token.BUFFER_TYPE: token.type = Token.SQL_TYPE; if (setBuf(token)) { stdprintln( SqltoolRB.input_movedtobuffer.getString()); } continue; case Token.SQL_TYPE: if (token.val == null) token.val = ""; setBuf(token); historize(); processSQL(); continue; default: throw new RuntimeException( "Internal assertion failed. " + "Unexpected token type: " + token.getTypeString()); } } catch (BadSpecial bs) { // BadSpecials ALWAYS have non-null getMessage(). if (token == null) { errprintln(SqltoolRB.errorat.getString( inputStreamLabel, "?", "?", bs.getMessage())); } else { errprintln(SqltoolRB.errorat.getString( inputStreamLabel, Integer.toString(token.line), token.reconstitute(), bs.getMessage(), bs.getMessage())); } Throwable cause = bs.getCause(); if (cause != null) { errprintln(SqltoolRB.causereport.getString( cause.toString())); } if (!continueOnError) { throw new SqlToolError(bs); } } catch (SQLException se) { //se.printStackTrace(); errprintln("SQL " + SqltoolRB.errorat.getString( inputStreamLabel, ((token == null) ? "?" : Integer.toString(token.line)), lastSqlStatement, se.getMessage())); // It's possible that we could have // SQLException.getMessage() == null, but if so, I think // it reasonable to show "null". That's a DB inadequacy. if (!continueOnError) { throw se; } } catch (BreakException be) { String msg = be.getMessage(); if (recursed) { rollbackUncoms = false; // Recursion level will exit by rethrowing the BE. // We set rollbackUncoms to false because only the // top level should detect break errors and // possibly roll back. } else if (msg == null || msg.equals("file")) { break; } else { errprintln(SqltoolRB.break_unsatisfied.getString(msg)); } if (recursed ||!continueOnError) { throw be; } } catch (ContinueException ce) { String msg = ce.getMessage(); if (recursed) { rollbackUncoms = false; } else { errprintln(SqltoolRB.continue_unsatisfied.getString(msg)); } if (recursed ||!continueOnError) { throw ce; } } catch (QuitNow qn) { throw qn; } catch (SqlToolError ste) { StringBuffer sb = new StringBuffer(SqltoolRB.errorat.getString( /* WARNING: I have removed an extra LS appended to * non-null ste.getMessages() below because I believe that * it is unnecessary (and causes inconsistent blank lines * to be written). * If I am wrong and this is needed for Scanner display or * something, restore it. */ ((token == null) ? (new String[] { inputStreamLabel, "?", "?", ((ste.getMessage() == null) ? "" : ste.getMessage()) }) : (new String[] { inputStreamLabel, Integer.toString(token.line), ((token.val == null) ? "" : token.reconstitute()), ((ste.getMessage() == null) ? "" : ste.getMessage()) })) )); Throwable cause = ste.getCause(); errprintln((cause == null) ? sb.toString() : SqltoolRB.causereport.getString(cause.toString())); if (!continueOnError) { throw ste; } } rollbackUncoms = false; // Exiting gracefully, so don't roll back. } catch (IOException ioe) { throw new SqlToolError( SqltoolRB.primaryinput_accessfail.getString(), ioe); } catch (QuitNow qn) { if (recursed) { throw qn; // Will rollback if conditions otherwise require. // Otherwise top level will decide based upon qn.getMessage(). } rollbackUncoms = (qn.getMessage() != null); if (rollbackUncoms) { errprintln(SqltoolRB.aborting.getString(qn.getMessage())); throw new SqlToolError(qn.getMessage()); } return; } finally { if (fetchingVar != null) { errprintln(SqltoolRB.plvar_set_incomplete.getString( fetchingVar)); rollbackUncoms = true; } if (shared.jdbcConn != null) { if (shared.jdbcConn.getAutoCommit()) shared.possiblyUncommitteds = false; if (rollbackUncoms && shared.possiblyUncommitteds) { errprintln(SqltoolRB.rollingback.getString()); shared.jdbcConn.rollback(); shared.possiblyUncommitteds = false; } } } } /** * Utility nested Exception class for internal use only. * * Do not instantiate with null message. */ private static class BadSpecial extends AppendableException { static final long serialVersionUID = 7162440064026570590L; BadSpecial(String s) { super(s); if (s == null) throw new RuntimeException( "Must construct BadSpecials with non-null message"); } BadSpecial(String s, Throwable t) { super(s, t); if (s == null) throw new RuntimeException( "Must construct BadSpecials with non-null message"); } } /** * Utility nested Exception class for internal use. * This must extend SqlToolError because it has to percolate up from * recursions of SqlTool.execute(), yet SqlTool.execute() is public. * Therefore, external users have no reason to specifically handle * QuitNow. */ private class QuitNow extends SqlToolError { static final long serialVersionUID = 1811094258670900488L; public QuitNow(String s) { super(s); } public QuitNow() { super(); } } /** * Utility nested Exception class for internal use. * This must extend SqlToolError because it has to percolate up from * recursions of SqlTool.execute(), yet SqlTool.execute() is public. * Therefore, external users have no reason to specifically handle * BreakException. */ private class BreakException extends SqlToolError { static final long serialVersionUID = 351150072817675994L; public BreakException() { super(); } public BreakException(String s) { super(s); } } /** * Utility nested Exception class for internal use. * This must extend SqlToolError because it has to percolate up from * recursions of SqlTool.execute(), yet SqlTool.execute() is public. * Therefore, external users have no reason to specifically handle * ContinueException. */ private class ContinueException extends SqlToolError { static final long serialVersionUID = 5064604160827106014L; public ContinueException() { super(); } public ContinueException(String s) { super(s); } } /** * Utility nested Exception class for internal use only. */ private class BadSubst extends Exception { static final long serialVersionUID = 7325933736897253269L; BadSubst(String s) { super(s); } } /** * Utility nested Exception class for internal use only. */ private class RowError extends AppendableException { static final long serialVersionUID = 754346434606022750L; RowError(String s) { super(s); } /* Unused so far RowError(Throwable t) { this(null, t); } */ RowError(String s, Throwable t) { super(s, t); } } /** * Process a Buffer/History Command. * * Due to the nature of the goal here, we don't trim() "other" like * we do for other kinds of commands. * * @param inString Complete command, less the leading ':' character. * @throws SQLException thrown by JDBC driver. * @throws BadSpecial special-command-specific errors. * @throws SqlToolError all other errors. */ private void processBuffHist(Token token) throws BadSpecial, SQLException, SqlToolError { if (token.val.length() < 1) { throw new BadSpecial(SqltoolRB.bufhist_unspecified.getString()); } // First handle the simple cases where user may not specify a // command number. char commandChar = token.val.charAt(0); String other = token.val.substring(1); if (other.trim().length() == 0) { other = null; } switch (commandChar) { case 'l' : case 'b' : enforce1charBH(other, 'l'); if (buffer == null) { stdprintln(nobufferYetString); } else { stdprintln(SqltoolRB.editbuffer_contents.getString( buffer.reconstitute())); } return; case 'h' : enforce1charBH(other, 'h'); showHistory(); return; case '?' : stdprintln(SqltoolRB.buffer_help.getString()); return; } Integer histNum = null; Matcher hm = slashHistoryPattern.matcher(token.val); if (hm.matches()) { histNum = historySearch(hm.group(1)); if (histNum == null) { stdprintln(SqltoolRB.substitution_nomatch.getString()); return; } } else { hm = historyPattern.matcher(token.val); if (!hm.matches()) { throw new BadSpecial(SqltoolRB.edit_malformat.getString()); // Empirically, I find that this pattern always captures two // groups. Unfortunately, there's no way to guarantee that :( . } histNum = ((hm.group(1) == null || hm.group(1).length() < 1) ? null : Integer.valueOf(hm.group(1))); } if (hm.groupCount() != 2) { throw new BadSpecial(SqltoolRB.edit_malformat.getString()); // Empirically, I find that this pattern always captures two // groups. Unfortunately, there's no way to guarantee that :( . } commandChar = ((hm.group(2) == null || hm.group(2).length() < 1) ? '\0' : hm.group(2).charAt(0)); other = ((commandChar == '\0') ? null : hm.group(2).substring(1)); if (other != null && other.length() < 1) other = null; Token targetCommand = ((histNum == null) ? null : commandFromHistory(histNum.intValue())); // Every command below depends upon buffer content. switch (commandChar) { case '\0' : // Special token set above. Just history recall. setBuf(targetCommand); stdprintln(SqltoolRB.buffer_restored.getString( buffer.reconstitute())); return; case ';' : enforce1charBH(other, ';'); if (targetCommand != null) setBuf(targetCommand); if (buffer == null) throw new BadSpecial( SqltoolRB.nobuffer_yet.getString()); stdprintln(SqltoolRB.buffer_executing.getString( buffer.reconstitute())); preempt = true; return; case 'a' : if (targetCommand == null) targetCommand = buffer; if (targetCommand == null) throw new BadSpecial( SqltoolRB.nobuffer_yet.getString()); boolean doExec = false; if (other != null) { if (other.trim().charAt(other.trim().length() - 1) == ';') { other = other.substring(0, other.lastIndexOf(';')); if (other.trim().length() < 1) throw new BadSpecial( SqltoolRB.append_empty.getString()); doExec = true; } } Token newToken = new Token(targetCommand.type, targetCommand.val, targetCommand.line); if (other != null) newToken.val += other; setBuf(newToken); if (doExec) { stdprintln(SqltoolRB.buffer_executing.getString( buffer.reconstitute())); preempt = true; return; } if (interactive) scanner.setMagicPrefix( newToken.reconstitute()); switch (newToken.type) { case Token.SQL_TYPE: scanner.setRequestedState(SqlFileScanner.SQL); break; case Token.SPECIAL_TYPE: scanner.setRequestedState(SqlFileScanner.SPECIAL); break; case Token.PL_TYPE: scanner.setRequestedState(SqlFileScanner.PL); break; default: throw new RuntimeException( "Internal assertion failed. " + "Appending to unexpected type: " + newToken.getTypeString()); } scanner.setCommandBuffer(newToken.val); return; case 'w' : if (targetCommand == null) targetCommand = buffer; if (targetCommand == null) throw new BadSpecial( SqltoolRB.nobuffer_yet.getString()); if (other == null) { throw new BadSpecial(SqltoolRB.destfile_demand.getString()); } String targetFile = dereference(other.trim(), false); // Dereference and trim the target file name // This is the only case where we dereference a : command. PrintWriter pw = null; try { pw = new PrintWriter( new OutputStreamWriter( new FileOutputStream(targetFile, true), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding) // Appendmode so can append to an SQL script. ); pw.println(targetCommand.reconstitute(true)); pw.flush(); } catch (Exception e) { throw new BadSpecial(SqltoolRB.file_appendfail.getString( targetFile), e); } finally { if (pw != null) { try { pw.close(); } finally { pw = null; // Encourage GC of buffers } } } return; case 's' : boolean modeExecute = false; boolean modeGlobal = false; if (targetCommand == null) targetCommand = buffer; if (targetCommand == null) throw new BadSpecial( SqltoolRB.nobuffer_yet.getString()); try { if (other == null || other.length() < 3) { throw new BadSubst( SqltoolRB.substitution_malformat.getString()); } Matcher m = substitutionPattern.matcher(other); if (!m.matches()) { throw new BadSubst( SqltoolRB.substitution_malformat.getString()); } // Note that this pattern does not include the leading :. if (m.groupCount() < 3 || m.groupCount() > 4) { throw new RuntimeException( "Internal assertion failed. " + "Matched substitution " + "pattern, but captured " + m.groupCount() + " groups"); } String optionGroup = ( (m.groupCount() > 3 && m.group(4) != null) ? (new String(m.group(4))) : null); if (optionGroup != null) { if (optionGroup.indexOf(';') > -1) { modeExecute = true; optionGroup = optionGroup.replaceFirst(";", ""); } if (optionGroup.indexOf('g') > -1) { modeGlobal = true; optionGroup = optionGroup.replaceFirst("g", ""); } } Matcher bufferMatcher = Pattern.compile("(?s" + ((optionGroup == null) ? "" : optionGroup) + ')' + m.group(2)).matcher(targetCommand.val); Token newBuffer = new Token(targetCommand.type, (modeGlobal ? bufferMatcher.replaceAll(m.group(3)) : bufferMatcher.replaceFirst(m.group(3))), targetCommand.line); if (newBuffer.val.equals(targetCommand.val)) { stdprintln(SqltoolRB.substitution_nomatch.getString()); return; } setBuf(newBuffer); stdprintln(modeExecute ? SqltoolRB.buffer_executing.getString( buffer.reconstitute()) : SqltoolRB.editbuffer_contents.getString( buffer.reconstitute()) ); } catch (PatternSyntaxException pse) { throw new BadSpecial( SqltoolRB.substitution_syntax.getString(), pse); } catch (BadSubst badswitch) { throw new BadSpecial( SqltoolRB.substitution_syntax.getString()); } if (modeExecute) preempt = true; return; } throw new BadSpecial(SqltoolRB.buffer_unknown.getString( Character.toString(commandChar))); } private boolean doPrepare; private String prepareVar; private String dsvColDelim; private String dsvColSplitter; private String dsvSkipPrefix; private String dsvRowDelim; private String dsvRowSplitter; private String dsvSkipCols; private boolean dsvTrimAll; private static String DSV_X_SYNTAX_MSG; private static String DSV_M_SYNTAX_MSG; private static String nobufferYetString; private void enforce1charSpecial(String tokenString, char command) throws BadSpecial { if (tokenString.length() != 1) { throw new BadSpecial(SqltoolRB.special_extrachars.getString( Character.toString(command), tokenString.substring(1))); } } private void enforce1charBH(String tokenString, char command) throws BadSpecial { if (tokenString != null) { throw new BadSpecial(SqltoolRB.buffer_extrachars.getString( Character.toString(command), tokenString)); } } /** * Process a Special Command. * * @param inString TRIMMED, no-null command (without leading \), * or null to operate on buffer. * @throws SQLException thrown by JDBC driver. * @throws BadSpecial special-command-specific errors. * @throws SqlToolError all other errors, plus QuitNow, * BreakException, ContinueException. */ private void processSpecial(String inString) throws BadSpecial, QuitNow, SQLException, SqlToolError { String string = (inString == null) ? buffer.val : inString; if (string.length() < 1) { throw new BadSpecial(SqltoolRB.special_unspecified.getString()); } Matcher m = specialPattern.matcher( plMode ? dereference(string, false) : string); if (!m.matches()) { throw new BadSpecial(SqltoolRB.special_malformat.getString()); // I think it's impossible to get here, since the pattern is // so liberal. } if (m.groupCount() < 1 || m.groupCount() > 2) { // Failed assertion throw new RuntimeException( "Internal assertion failed. Pattern matched, yet captured " + m.groupCount() + " groups"); } String arg1 = m.group(1); String other = ((m.groupCount() > 1) ? m.group(2) : null); switch (arg1.charAt(0)) { case 'q' : enforce1charSpecial(arg1, 'q'); if (other != null) { throw new QuitNow(other); } throw new QuitNow(); case 'H' : enforce1charSpecial(arg1, 'H'); htmlMode = !htmlMode; stdprintln(SqltoolRB.html_mode.getString( Boolean.toString(htmlMode))); return; case 'm' : if (arg1.equals("m?") || (arg1.equals("m") && other != null && other.equals("?"))) { stdprintln(DSV_OPTIONS_TEXT + LS + DSV_M_SYNTAX_MSG); return; } if (arg1.length() != 1 || other == null) { throw new BadSpecial(DSV_M_SYNTAX_MSG); } boolean noComments = other.charAt(other.length() - 1) == '*'; String skipPrefix = null; if (noComments) { other = other.substring(0, other.length()-1).trim(); if (other.length() < 1) { throw new BadSpecial(DSV_M_SYNTAX_MSG); } } else { skipPrefix = dsvSkipPrefix; } int colonIndex = other.indexOf(" :"); if (colonIndex > -1 && colonIndex < other.length() - 2) { skipPrefix = other.substring(colonIndex + 2); other = other.substring(0, colonIndex).trim(); } importDsv(other, skipPrefix); return; case 'x' : requireConnection(); if (arg1.equals("x?") || (arg1.equals("x") && other != null && other.equals("?"))) { stdprintln(DSV_OPTIONS_TEXT + LS + DSV_X_SYNTAX_MSG); return; } try { if (arg1.length() != 1 || other == null) { throw new BadSpecial(DSV_X_SYNTAX_MSG); } String tableName = ((other.indexOf(' ') > 0) ? null : other); if (dsvTargetFile == null && tableName == null) { throw new BadSpecial( SqltoolRB.dsv_targetfile_demand.getString()); } File dsvFile = new File((dsvTargetFile == null) ? (tableName + ".dsv") : dsvTargetFile); pwDsv = new PrintWriter(new OutputStreamWriter( new FileOutputStream(dsvFile), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding)); ResultSet rs = shared.jdbcConn.createStatement() .executeQuery((tableName == null) ? other : ("SELECT * FROM " + tableName)); try { List<Integer> colList = new ArrayList<Integer>(); int[] incCols = null; if (dsvSkipCols != null) { Set<String> skipCols = new HashSet<String>(); for (String s : dsvSkipCols.split(dsvColDelim, -1)) { // Don't know if better to use dsvColDelim or // dsvColSplitter. Going with former, since the // latter should not need to be set for eXporting // (only importing). skipCols.add(s.trim().toLowerCase()); } ResultSetMetaData rsmd = rs.getMetaData(); for (int i = 1; i <= rsmd.getColumnCount(); i++) { if (!skipCols.remove(rsmd.getColumnName(i) .toLowerCase())) { colList.add(Integer.valueOf(i)); } } if (colList.size() < 1) { throw new BadSpecial( SqltoolRB.dsv_nocolsleft.getString( dsvSkipCols)); } if (skipCols.size() > 0) { throw new BadSpecial( SqltoolRB.dsv_skipcols_missing.getString( skipCols.toString())); } incCols = new int[colList.size()]; for (int i = 0; i < incCols.length; i++) { incCols[i] = colList.get(i).intValue(); } } displayResultSet(null, rs, incCols, null); } finally { rs.close(); } pwDsv.flush(); stdprintln(SqltoolRB.file_wrotechars.getString( Long.toString(dsvFile.length()), dsvFile.toString())); } catch (FileNotFoundException e) { throw new BadSpecial(SqltoolRB.file_writefail.getString( other), e); } catch (UnsupportedEncodingException e) { throw new BadSpecial(SqltoolRB.file_writefail.getString( other), e); } finally { // Reset all state changes if (pwDsv != null) { try { pwDsv.close(); } finally { pwDsv = null; // Encourage GC of buffers } } } return; case 'd' : requireConnection(); if (arg1.equals("d?") || (arg1.equals("d") && other != null && other.equals("?"))) { stdprintln(D_OPTIONS_TEXT); return; } if (arg1.length() == 2) { listTables(arg1.charAt(1), other); return; } if (arg1.length() == 1 && other != null) try { int space = other.indexOf(' '); if (space < 0) { describe(other, null); } else { describe(other.substring(0, space), other.substring(space + 1).trim()); } return; } catch (SQLException se) { throw new BadSpecial( SqltoolRB.metadata_fetch_fail.getString(), se); } throw new BadSpecial(SqltoolRB.special_d_like.getString()); case 'o' : enforce1charSpecial(arg1, 'o'); if (other == null) { if (pwQuery == null) { throw new BadSpecial( SqltoolRB.outputfile_nonetoclose.getString()); } closeQueryOutputStream(); return; } if (pwQuery != null) { stdprintln(SqltoolRB.outputfile_reopening.getString()); closeQueryOutputStream(); } try { pwQuery = new PrintWriter(new OutputStreamWriter( new FileOutputStream(other, true), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding)); /* Opening in append mode, so it's possible that we will * be adding superfluous <HTML> and <BODY> tags. * I think that browsers can handle that */ pwQuery.println((htmlMode ? ("<HTML>" + LS + "<!--") : "#") + " " + (new java.util.Date()) + ". " + SqltoolRB.outputfile_header.getString( getClass().getName()) + (htmlMode ? (" -->" + LS + LS + "<BODY>") : LS)); pwQuery.flush(); } catch (Exception e) { throw new BadSpecial(SqltoolRB.file_writefail.getString( other), e); } return; case 'i' : enforce1charSpecial(arg1, 'i'); if (other == null) { throw new BadSpecial( SqltoolRB.sqlfile_name_demand.getString()); } try { new SqlFile(this, new File(other)).execute(); } catch (ContinueException ce) { throw ce; } catch (BreakException be) { String beMessage = be.getMessage(); // Handle "file" and plain breaks (by doing nothing) if (beMessage != null &&!beMessage.equals("file")) { throw be; } } catch (QuitNow qn) { throw qn; } catch (Exception e) { throw new BadSpecial( SqltoolRB.sqlfile_execute_fail.getString(other), e); } return; case 'p' : enforce1charSpecial(arg1, 'p'); if (other == null) { stdprintln(true); } else { stdprintln(other, true); } return; case 'l' : if ((arg1.equals("l?") && other == null) || (arg1.equals("l") && other != null && other.equals("?"))) { stdprintln(SqltoolRB.log_syntax.getString()); } else { enforce1charSpecial(arg1, 'l'); Matcher logMatcher = ((other == null) ? null : logPattern.matcher(other.trim())); if (logMatcher == null || (!logMatcher.matches())) throw new BadSpecial( SqltoolRB.log_syntax_error.getString()); String levelString = logMatcher.group(1); Level level = null; if (levelString.equalsIgnoreCase("FINER")) level = Level.FINER; else if (levelString.equalsIgnoreCase("WARNING")) level = Level.WARNING; else if (levelString.equalsIgnoreCase("SEVERE")) level = Level.SEVERE; else if (levelString.equalsIgnoreCase("INFO")) level = Level.INFO; else if (levelString.equalsIgnoreCase("FINEST")) level = Level.FINEST; if (level == null) throw new RuntimeException( "Internal assertion failed. " + " Unexpected Level string: " + levelString); logger.enduserlog(level, logMatcher.group(2)); } return; case 'a' : requireConnection(); enforce1charSpecial(arg1, 'a'); if (other != null) { shared.jdbcConn.setAutoCommit( Boolean.parseBoolean(other)); shared.possiblyUncommitteds = false; } stdprintln(SqltoolRB.a_setting.getString( Boolean.toString(shared.jdbcConn.getAutoCommit()))); return; case 'j' : try { enforce1charSpecial(arg1, 'j'); String urlid = null; String acct = null; String pwd = null; String url = null; boolean goalAutoCommit = false; String[] tokens = (other == null) ? (new String[0]) : other.split("\\s+", 3); switch (tokens.length) { case 0: break; case 1: urlid = tokens[0]; break; case 2: acct = tokens[0]; pwd = ""; // default password to "" url = tokens[1]; break; case 3: acct = tokens[0]; pwd = tokens[1]; url = tokens[2]; break; } if (tokens.length > 0) { // Close current connection if (shared.jdbcConn != null) try { goalAutoCommit = shared.jdbcConn.getAutoCommit(); shared.jdbcConn.close(); shared.possiblyUncommitteds = false; shared.jdbcConn = null; stdprintln(SqltoolRB.disconnect_success.getString()); } catch (SQLException se) { throw new BadSpecial( SqltoolRB.disconnect_failure.getString(), se); } } if (urlid != null || acct != null) try { if (urlid != null) { shared.jdbcConn = new RCData(new File( SqlTool.DEFAULT_RCFILE), urlid).getConnection(); } else if (acct != null) { shared.jdbcConn = DriverManager.getConnection(url, acct, pwd); } shared.possiblyUncommitteds = false; shared.jdbcConn.setAutoCommit(goalAutoCommit); } catch (Exception e) { throw new BadSpecial("Failed to connect", e); } displayConnBanner(); } catch (Throwable t) { t.printStackTrace(); return; } return; case 'v' : requireConnection(); enforce1charSpecial(arg1, 'v'); if (other != null) { if (integerPattern.matcher(other).matches()) { shared.jdbcConn.setTransactionIsolation( Integer.parseInt(other)); } else { RCData.setTI(shared.jdbcConn, other); } } stdprintln(SqltoolRB.transiso_report.getString( (shared.jdbcConn.isReadOnly() ? "R/O " : "R/W "), RCData.tiToString( shared.jdbcConn.getTransactionIsolation()))); return; case '=' : requireConnection(); enforce1charSpecial(arg1, '='); shared.jdbcConn.commit(); shared.possiblyUncommitteds = false; stdprintln(SqltoolRB.committed.getString()); return; case 'b' : if (arg1.length() == 1) { if (other != null) { throw new BadSpecial( SqltoolRB.special_b_malformat.getString()); } fetchBinary = true; return; } if (arg1.charAt(1) == 'p') { if (other != null) { throw new BadSpecial( SqltoolRB.special_b_malformat.getString()); } doPrepare = true; return; } if ((arg1.charAt(1) != 'd' && arg1.charAt(1) != 'l') || other == null) { throw new BadSpecial( SqltoolRB.special_b_malformat.getString()); } File otherFile = new File(other); try { if (arg1.charAt(1) == 'd') { dump(otherFile); } else { binBuffer = SqlFile.loadBinary(otherFile); stdprintln(SqltoolRB.binary_loadedbytesinto.getString( binBuffer.length)); } } catch (BadSpecial bs) { throw bs; } catch (IOException ioe) { throw new BadSpecial(SqltoolRB.binary_filefail.getString( other), ioe); } return; case 't' : enforce1charSpecial(arg1, '='); if (other != null) { // But remember that we have to abort on some I/O errors. reportTimes = Boolean.parseBoolean(other); } stdprintln(SqltoolRB.exectime_reporting.getString( Boolean.toString(reportTimes))); return; case '*' : case 'c' : enforce1charSpecial(arg1, '='); if (other != null) { // But remember that we have to abort on some I/O errors. continueOnError = Boolean.parseBoolean(other); } stdprintln(SqltoolRB.c_setting.getString( Boolean.toString(continueOnError))); return; case '?' : stdprintln(SqltoolRB.special_help.getString()); return; case '!' : /* N.b. This DOES NOT HANDLE UNIX shell wildcards, since there * is no UNIX shell involved. * Doesn't make sense to incur overhead of a shell without * stdin capability. * Could pipe System.in to the forked process, but that's * probably not worth the effort due to Java's terrible * and inescapable System.in buffering. I.e., the forked * program or shell wouldn't get stdin until user hits Enter. * * I'd like to execute the user's default shell if they * ran "\!" with no argument, but (a) there is no portable * way to determine the user's default or login shell; and * (b) shell is useless without stdin ability. */ InputStream stream; byte[] ba = new byte[1024]; String extCommand = ((arg1.length() == 1) ? "" : arg1.substring(1)) + ((arg1.length() > 1 && other != null) ? " " : "") + ((other == null) ? "" : other); if (extCommand.trim().length() < 1) throw new BadSpecial(SqltoolRB.bang_incomplete.getString()); Process proc = null; try { Runtime runtime = Runtime.getRuntime(); proc = ((wincmdPattern == null) ? runtime.exec(extCommand) : runtime.exec(genWinArgs(extCommand)) ); proc.getOutputStream().close(); int i; stream = proc.getInputStream(); while ((i = stream.read(ba)) > 0) { stdprint(new String(ba, 0, i)); } stream.close(); stream = proc.getErrorStream(); String s; while ((i = stream.read(ba)) > 0) { s = new String(ba, 0, i); if (s.endsWith(LS)) { // This block just prevents logging of // double-line-breaks. if (s.length() == LS.length()) continue; s = s.substring(0, s.length() - LS.length()); } logger.severe(s); } stream.close(); stream = null; // Encourage buffer GC if (proc.waitFor() != 0) { throw new BadSpecial( SqltoolRB.bang_command_fail.getString( extCommand)); } } catch (BadSpecial bs) { throw bs; } catch (Exception e) { throw new BadSpecial(SqltoolRB.bang_command_fail.getString( extCommand), e); } finally { if (proc != null) { proc.destroy(); } } return; } throw new BadSpecial(SqltoolRB.special_unknown.getString( Character.toString(arg1.charAt(0)))); } private static final char[] nonVarChars = { ' ', '\t', '=', '}', '\n', '\r', '\f' }; /** * Returns index specifying 1 past end of a variable name. * * @param inString String containing a variable name * @param startIndex Index within inString where the variable name begins * @return Index within inString, 1 past end of the variable name */ static int pastName(String inString, int startIndex) { String workString = inString.substring(startIndex); int e = inString.length(); // Index 1 past end of var name. int nonVarIndex; for (char nonVarChar : nonVarChars) { nonVarIndex = workString.indexOf(nonVarChar); if (nonVarIndex > -1 && nonVarIndex < e) { e = nonVarIndex; } } return startIndex + e; } /** * Deference *{} PL variables and ${} System Property variables. * * @throws SqlToolError */ private String dereference(String inString, boolean permitAlias) throws SqlToolError { if (inString.length() < 1) return inString; /* TODO: Rewrite using java.util.regex. */ String varName, varValue; StringBuffer expandBuffer = new StringBuffer(inString); int b, e; // begin and end of name. end really 1 PAST name int iterations; if (permitAlias && inString.trim().charAt(0) == '/') { int slashIndex = inString.indexOf('/'); e = SqlFile.pastName(inString.substring(slashIndex + 1), 0); // In this case, e is the exact length of the var name. if (e < 1) { throw new SqlToolError(SqltoolRB.plalias_malformat.getString()); } varName = inString.substring(slashIndex + 1, slashIndex + 1 + e); varValue = shared.userVars.get(varName); if (varValue == null) { throw new SqlToolError( SqltoolRB.plvar_undefined.getString(varName)); } expandBuffer.replace(slashIndex, slashIndex + 1 + e, shared.userVars.get(varName)); } String s; boolean permitUnset; // Permit unset with: ${:varname} // Prohibit unset with : ${varnam} iterations = 0; while (true) { s = expandBuffer.toString(); b = s.indexOf("${"); if (b < 0) { // No more unexpanded variable uses break; } e = s.indexOf('}', b + 2); if (e == b + 2) { throw new SqlToolError(SqltoolRB.sysprop_empty.getString()); } if (e < 0) { throw new SqlToolError( SqltoolRB.sysprop_unterminated.getString()); } permitUnset = (s.charAt(b + 2) == ':'); varName = s.substring(b + (permitUnset ? 3 : 2), e); if (iterations++ > 10000) throw new SqlToolError(SqltoolRB.var_infinite.getString(varName)); varValue = System.getProperty(varName); if (varValue == null) { if (permitUnset) { varValue = ""; } else { throw new SqlToolError( SqltoolRB.sysprop_undefined.getString(varName)); } } expandBuffer.replace(b, e + 1, varValue); } iterations = 0; while (true) { s = expandBuffer.toString(); b = s.indexOf("*{"); if (b < 0) { // No more unexpanded variable uses break; } e = s.indexOf('}', b + 2); if (e == b + 2) { throw new SqlToolError(SqltoolRB.plvar_nameempty.getString()); } if (e < 0) { throw new SqlToolError( SqltoolRB.plvar_unterminated.getString()); } permitUnset = (s.charAt(b + 2) == ':'); varName = s.substring(b + (permitUnset ? 3 : 2), e); if (iterations++ > 100000) throw new SqlToolError( SqltoolRB.var_infinite.getString(varName)); // TODO: Use a smarter algorithm to handle (or prohibit) // recursion without this clumsy detection tactic. varValue = shared.userVars.get(varName); if (varValue == null) { if (permitUnset) { varValue = ""; } else { throw new SqlToolError( SqltoolRB.plvar_undefined.getString(varName)); } } expandBuffer.replace(b, e + 1, varValue); } return expandBuffer.toString(); } private boolean plMode; // PL variable name currently awaiting query output. private String fetchingVar; private boolean silentFetch; private boolean fetchBinary; /** * Process a block PL command like "if" of "foreach". */ private void processBlock(Token token) throws BadSpecial, SqlToolError { Matcher m = plPattern.matcher(dereference(token.val, false)); if (!m.matches()) { throw new BadSpecial(SqltoolRB.pl_malformat.getString()); // I think it's impossible to get here, since the pattern is // so liberal. } if (m.groupCount() < 1 || m.group(1) == null) { plMode = true; stdprintln(SqltoolRB.pl_expansionmode.getString("on")); return; } String[] tokens = m.group(1).split("\\s+", -1); // If user runs any PL command, we turn PL mode on. plMode = true; if (tokens[0].equals("foreach")) { Matcher foreachM = foreachPattern.matcher( dereference(token.val, false)); if (!foreachM.matches()) { throw new BadSpecial(SqltoolRB.foreach_malformat.getString()); } if (foreachM.groupCount() != 2) { throw new RuntimeException( "Internal assertion failed. " + "foreach pattern matched, but captured " + foreachM.groupCount() + " groups"); } String varName = foreachM.group(1); if (varName.indexOf(':') > -1) { throw new BadSpecial(SqltoolRB.plvar_nocolon.getString()); } String[] values = foreachM.group(2).split("\\s+", -1); String origval = shared.userVars.get(varName); try { for (String val : values) { try { shared.userVars.put(varName, val); updateUserSettings(); boolean origRecursed = recursed; recursed = true; try { scanpass(token.nestedBlock.dup()); } finally { recursed = origRecursed; } } catch (ContinueException ce) { String ceMessage = ce.getMessage(); if (ceMessage != null &&!ceMessage.equals("foreach")) { throw ce; } } } } catch (BreakException be) { String beMessage = be.getMessage(); // Handle "foreach" and plain breaks (by doing nothing) if (beMessage != null &&!beMessage.equals("foreach")) { throw be; } } catch (QuitNow qn) { throw qn; } catch (RuntimeException re) { throw re; // Unrecoverable } catch (Exception e) { throw new BadSpecial(SqltoolRB.pl_block_fail.getString(), e); } if (origval == null) { shared.userVars.remove(varName); updateUserSettings(); } else { shared.userVars.put(varName, origval); } return; } if (tokens[0].equals("if") || tokens[0].equals("while")) { Matcher ifwhileM= ifwhilePattern.matcher( dereference(token.val, false)); if (!ifwhileM.matches()) { throw new BadSpecial(SqltoolRB.ifwhile_malformat.getString()); } if (ifwhileM.groupCount() != 1) { throw new RuntimeException( "Internal assertion failed. " + "if/while pattern matched, but captured " + ifwhileM.groupCount() + " groups"); } String[] values = ifwhileM.group(1).replaceAll("!([a-zA-Z0-9*])", "! $1"). replaceAll("([a-zA-Z0-9*])!", "$1 !").split("\\s+", -1); if (tokens[0].equals("if")) { try { if (eval(values)) { boolean origRecursed = recursed; recursed = true; try { scanpass(token.nestedBlock.dup()); } finally { recursed = origRecursed; } } } catch (BreakException be) { String beMessage = be.getMessage(); // Handle "if" and plain breaks (by doing nothing) if (beMessage == null ||!beMessage.equals("if")) { throw be; } } catch (ContinueException ce) { throw ce; } catch (QuitNow qn) { throw qn; } catch (BadSpecial bs) { bs.appendMessage(SqltoolRB.if_malformat.getString()); throw bs; } catch (RuntimeException re) { throw re; // Unrecoverable } catch (Exception e) { throw new BadSpecial( SqltoolRB.pl_block_fail.getString(), e); } } else if (tokens[0].equals("while")) { try { while (eval(values)) { try { boolean origRecursed = recursed; recursed = true; try { scanpass(token.nestedBlock.dup()); } finally { recursed = origRecursed; } } catch (ContinueException ce) { String ceMessage = ce.getMessage(); if (ceMessage != null &&!ceMessage.equals("while")) { throw ce; } } } } catch (BreakException be) { String beMessage = be.getMessage(); // Handle "while" and plain breaks (by doing nothing) if (beMessage != null &&!beMessage.equals("while")) { throw be; } } catch (QuitNow qn) { throw qn; } catch (BadSpecial bs) { bs.appendMessage(SqltoolRB.while_malformat.getString()); throw bs; } catch (RuntimeException re) { throw re; // Unrecoverable } catch (Exception e) { throw new BadSpecial( SqltoolRB.pl_block_fail.getString(), e); } } else { // Assertion throw new RuntimeException( SqltoolRB.pl_unknown.getString(tokens[0])); } return; } throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0])); } /** * Process a Non-Block Process Language Command. * Nesting not supported yet. * * @param inString Trimmed non-null command without leading * * (may be empty string ""). * @throws BadSpecial special-command-specific errors. * @throws SqlToolError all other errors, plus BreakException and * ContinueException. */ private void processPL(String inString) throws BadSpecial, SqlToolError { String string = (inString == null) ? buffer.val : inString; Matcher m = plPattern.matcher(dereference(string, false)); if (!m.matches()) { throw new BadSpecial(SqltoolRB.pl_malformat.getString()); // I think it's impossible to get here, since the pattern is // so liberal. } if (m.groupCount() < 1 || m.group(1) == null) { plMode = true; stdprintln(SqltoolRB.pl_expansionmode.getString("on")); return; } String[] tokens = m.group(1).split("\\s+", -1); if (tokens[0].charAt(0) == '?') { stdprintln(SqltoolRB.pl_help.getString()); return; } // If user runs any PL command, we turn PL mode on. plMode = true; if (tokens[0].equals("end")) { throw new BadSpecial(SqltoolRB.end_noblock.getString()); } if (tokens[0].equals("continue")) { if (tokens.length > 1) { if (tokens.length == 2 && (tokens[1].equals("foreach") || tokens[1].equals("while"))) { throw new ContinueException(tokens[1]); } throw new BadSpecial(SqltoolRB.continue_syntax.getString()); } throw new ContinueException(); } if (tokens[0].equals("break")) { if (tokens.length > 1) { if (tokens.length == 2 && (tokens[1].equals("foreach") || tokens[1].equals("if") || tokens[1].equals("while") || tokens[1].equals("file"))) { throw new BreakException(tokens[1]); } throw new BadSpecial(SqltoolRB.break_syntax.getString()); } throw new BreakException(); } if (tokens[0].equals("list") || tokens[0].equals("listvalues") || tokens[0].equals("listsysprops")) { boolean sysProps =tokens[0].equals("listsysprops"); String s; boolean doValues = (tokens[0].equals("listvalues") || sysProps); // Always list System Property values. // They are unlikely to be very long, like PL variables may be. if (tokens.length == 1) { stdprint(formatNicely( (sysProps ? System.getProperties() : shared.userVars), doValues)); } else { if (doValues) { stdprintln(SqltoolRB.pl_list_parens.getString()); } else { stdprintln(SqltoolRB.pl_list_lengths.getString()); } for (String token : tokens) { s = (String) (sysProps ? System.getProperties() : shared.userVars).get(token); if (s == null) continue; stdprintln(" " + token + ": " + (doValues ? ("(" + s + ')') : Integer.toString(s.length()))); } } return; } if (tokens[0].equals("dump") || tokens[0].equals("load")) { if (tokens.length != 3) { throw new BadSpecial(SqltoolRB.dumpload_malformat.getString()); } String varName = tokens[1]; if (varName.indexOf(':') > -1) { throw new BadSpecial(SqltoolRB.plvar_nocolon.getString()); } File dlFile = new File(tokens[2]); try { if (tokens[0].equals("dump")) { dump(varName, dlFile); } else { load(varName, dlFile, shared.encoding); } } catch (IOException ioe) { throw new BadSpecial(SqltoolRB.dumpload_fail.getString( varName, dlFile.toString()), ioe); } return; } if (tokens[0].equals("prepare")) { if (tokens.length != 2) { throw new BadSpecial(SqltoolRB.prepare_malformat.getString()); } if (shared.userVars.get(tokens[1]) == null) { throw new BadSpecial( SqltoolRB.plvar_undefined.getString(tokens[1])); } prepareVar = tokens[1]; doPrepare = true; return; } m = varsetPattern.matcher(dereference(string, false)); if (!m.matches()) { throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0])); } if (m.groupCount() < 2 || m.groupCount() > 3) { // Assertion throw new RuntimeException("varset pattern matched but captured " + m.groupCount() + " groups"); } String varName = m.group(1); if (varName.indexOf(':') > -1) { throw new BadSpecial(SqltoolRB.plvar_nocolon.getString()); } switch (m.group(2).charAt(0)) { case '_' : silentFetch = true; case '~' : if (m.groupCount() > 2 && m.group(3) != null) { throw new BadSpecial(SqltoolRB.plvar_tildedash_nomoreargs.getString( m.group(3))); } shared.userVars.remove(varName); updateUserSettings(); fetchingVar = varName; return; case '=' : if (fetchingVar != null && fetchingVar.equals(varName)) { fetchingVar = null; } if (varName.equals("*ENCODING")) try { // Special case so we can proactively prohibit encodings // which will not work, so we'll always be confident // that 'encoding' value is always good. setEncoding(m.group(3)); return; } catch (UnsupportedEncodingException use) { throw new BadSpecial( SqltoolRB.encode_fail.getString(m.group(3))); } if (m.groupCount() > 2 && m.group(3) != null) { shared.userVars.put(varName, m.group(3)); } else { shared.userVars.remove(varName); } updateUserSettings(); return; } throw new BadSpecial(SqltoolRB.pl_unknown.getString(tokens[0])); // I think this would already be caught in the setvar block above. } /** * Wrapper methods so don't need to call x(..., false) in most cases. */ /* Unused. Enable when/if need. private void stdprintln() { stdprintln(false); } */ private void stdprint(String s) { stdprint(s, false); } private void stdprintln(String s) { stdprintln(s, false); } /** * Encapsulates normal output. * * Conditionally HTML-ifies output. */ private void stdprintln(boolean queryOutput) { if (shared.psStd != null) if (htmlMode) { shared.psStd.println("<BR>"); } else { shared.psStd.println(); } if (queryOutput && pwQuery != null) { if (htmlMode) { pwQuery.println("<BR>"); } else { pwQuery.println(); } pwQuery.flush(); } } /** * Encapsulates error output. * * Conditionally HTML-ifies error output. */ private void errprintln(String s) { if (shared.psStd != null && htmlMode) { shared.psStd.println("<DIV style='color:white; background: red; " + "font-weight: bold'>" + s + "</DIV>"); } else { logger.privlog(Level.SEVERE, s, null, 5, SqlFile.class); /* Only consistent way we can log source location is to log * the caller of SqlFile. * This seems acceptable, since the location being reported * here is not the source of the problem anyways. */ } } /** * Encapsulates normal output. * * Conditionally HTML-ifies output. */ private void stdprint(String s, boolean queryOutput) { if (shared.psStd != null) shared.psStd.print(htmlMode ? ("<P>" + s + "</P>") : s); if (queryOutput && pwQuery != null) { pwQuery.print(htmlMode ? ("<P>" + s + "</P>") : s); pwQuery.flush(); } } /** * Encapsulates normal output. * * Conditionally HTML-ifies output. */ private void stdprintln(String s, boolean queryOutput) { shared.psStd.println(htmlMode ? ("<P>" + s + "</P>") : s); if (queryOutput && pwQuery != null) { pwQuery.println(htmlMode ? ("<P>" + s + "</P>") : s); pwQuery.flush(); } } // Just because users may be used to seeing "[null]" in normal // SqlFile output, we use the same default value for null in DSV // files, but this DSV null representation can be changed to anything. private static final String DEFAULT_NULL_REP = "[null]"; private static final String DEFAULT_ROW_DELIM = LS; private static final String DEFAULT_ROW_SPLITTER = "\\r\\n|\\r|\\n"; private static final String DEFAULT_COL_DELIM = "|"; private static final String DEFAULT_COL_SPLITTER = "\\|"; private static final String DEFAULT_SKIP_PREFIX = "#"; private static final int DEFAULT_ELEMENT = 0, HSQLDB_ELEMENT = 1, ORACLE_ELEMENT = 2 ; // These do not specify order listed, just inclusion. private static final int[] listMDSchemaCols = { 1 }; private static final int[] listMDIndexCols = { 2, 6, 3, 9, 4, 10, 11 }; /** Column numbering starting at 1. */ private static final int[][] listMDTableCols = { { 2, 3 }, // Default { 2, 3 }, // HSQLDB { 2, 3 }, // Oracle }; /** * SYS and SYSTEM are the only base system accounts in Oracle, however, * from an empirical perspective, all of these other accounts are * system accounts because <UL> * <LI> they are hidden from the casual user * <LI> they are created by the installer at installation-time * <LI> they are used automatically by the Oracle engine when the * specific Oracle sub-product is used * <LI> the accounts should not be <I>messed with</I> by database users * <LI> the accounts should certainly not be used if the specific * Oracle sub-product is going to be used. * </UL> * * General advice: If you aren't going to use an Oracle sub-product, * then <B>don't install it!</B> * Don't blindly accept default when running OUI. * * If users also see accounts that they didn't create with names like * SCOTT, ADAMS, JONES, CLARK, BLAKE, OE, PM, SH, QS, QS_*, these * contain sample data and the schemas can safely be removed. */ private static final String[] oracleSysSchemas = { "SYS", "SYSTEM", "OUTLN", "DBSNMP", "OUTLN", "MDSYS", "ORDSYS", "ORDPLUGINS", "CTXSYS", "DSSYS", "PERFSTAT", "WKPROXY", "WKSYS", "WMSYS", "XDB", "ANONYMOUS", "ODM", "ODM_MTR", "OLAPSYS", "TRACESVR", "REPADMIN" }; public String getCurrentSchema() throws BadSpecial, SqlToolError { requireConnection(); Statement st = null; ResultSet rs = null; try { st = shared.jdbcConn.createStatement(); rs = st.executeQuery("VALUES CURRENT_SCHEMA"); if (!rs.next()) throw new BadSpecial(SqltoolRB.no_vendor_schemaspt.getString()); String currentSchema = rs.getString(1); if (currentSchema == null) throw new BadSpecial( SqltoolRB.schemaname_retrieval_fail.getString()); return currentSchema; } catch (SQLException se) { throw new BadSpecial(SqltoolRB.no_vendor_schemaspt.getString()); } finally { if (rs != null) try { rs.close(); } catch (SQLException se) { // Purposefully doing nothing } finally { rs = null; } if (st != null) try { st.close(); } catch (SQLException se) { // Purposefully doing nothing } finally { st = null; } } } /** * Lists available database tables. * * Filter handling is admittedly inconsistent, both wrt pattern * matching (java.util.regex vs. DB-implemented matching) and * which columns the filter is matched against. * The former is because, for performance and because the DB should * know best how to supply the desired results, we need to let the * database do filtering if at all possible. * In many cases, the DB does not have a filter option, so we have * to filter ourselves. * For the latter, we have no control over which columsn the DB * matches agains, plus the displayResultSet() method in this class * can only match against all columns (only reason not to add * column-specific filtering is to keep the complexity manageable). * * @throws BadSpecial usually wrap a cause (which cause is a * SQLException in some cases). * @throws SqlToolError passed through from other methods in this class. */ private void listTables(char c, String inFilter) throws BadSpecial, SqlToolError { requireConnection(); String schema = null; int[] listSet = null; String[] types = null; /** For workaround for \T for Oracle */ String[] additionalSchemas = null; /** This is for specific non-getTable() queries */ Statement statement = null; ResultSet rs = null; String narrower = ""; /* * Doing case-sensitive filters now, for greater portability. String filter = ((inFilter == null) ? null : inFilter.toUpperCase()); */ String filter = inFilter; try { DatabaseMetaData md = shared.jdbcConn.getMetaData(); String dbProductName = md.getDatabaseProductName(); int majorVersion = 0; int minorVersion = 0; // We only use majorVersion and minorVersion for HyperSQL so far // The calls avoided here avoid problems with non-confirmant drivers if (dbProductName.indexOf("HSQL") > -1) try { majorVersion = md.getDatabaseMajorVersion(); minorVersion = md.getDatabaseMinorVersion(); } catch (UnsupportedOperationException uoe) { // It seems that Sun's JDBC/ODBC bridge throws here majorVersion = 2; minorVersion = 0; } //System.err.println("DB NAME = (" + dbProductName + ')'); // Database-specific table filtering. /* 3 Types of actions: * 1) Special handling. Return from the "case" block directly. * 2) Execute a specific query. Set statement in the "case". * 3) Otherwise, set filter info for dbmd.getTable() in the * "case". */ types = new String[1]; switch (c) { case '*' : types = null; break; case 'S' : if (dbProductName.indexOf("Oracle") > -1) { errprintln(SqltoolRB.vendor_oracle_dS.getString()); types[0] = "TABLE"; schema = "SYS"; additionalSchemas = oracleSysSchemas; } else { types[0] = "SYSTEM TABLE"; } break; case 's' : if (dbProductName.indexOf("HSQL") > -1) { // HSQLDB does not consider Sequences as "tables", // hence we do not list them in // DatabaseMetaData.getTables(). if (filter != null) { Matcher matcher = dotPattern.matcher(filter); if (matcher.matches()) { filter = (matcher.group(2).length() > 0) ? matcher.group(2) : null; narrower = "\nWHERE sequence_schema = '" + ((matcher.group(1).length() > 0) ? matcher.group(1) : getCurrentSchema()) + "'"; } } statement = shared.jdbcConn.createStatement(); statement.execute( "SELECT sequence_schema, sequence_name FROM " + "information_schema." + ((minorVersion> 8 || majorVersion > 1) ? "sequences" : "system_sequences") + narrower); } else { types[0] = "SEQUENCE"; } break; case 'r' : if (dbProductName.indexOf("HSQL") > -1) { statement = shared.jdbcConn.createStatement(); statement.execute( "SELECT authorization_name FROM information_schema." + ((minorVersion> 8 || majorVersion > 1) ? "authorizations" : "system_authorizations") + "\nWHERE authorization_type = 'ROLE'\n" + "ORDER BY authorization_name"); } else if (dbProductName.indexOf( "Adaptive Server Enterprise") > -1) { // This is the basic Sybase server. Sybase also has // their "Anywhere", ASA (for embedded), and replication // databases, but I don't know the Metadata strings for // those. statement = shared.jdbcConn.createStatement(); statement.execute( "SELECT name FROM syssrvroles ORDER BY name"); } else if (dbProductName.indexOf( "Apache Derby") > -1) { throw new BadSpecial( SqltoolRB.vendor_derby_dr.getString()); } else { throw new BadSpecial( SqltoolRB.vendor_nosup_d.getString("r")); } break; case 'u' : if (dbProductName.indexOf("HSQL") > -1) { statement = shared.jdbcConn.createStatement(); statement.execute("SELECT " + ((minorVersion> 8 || majorVersion > 1) ? "user_name" : "user") + ", admin FROM " + "information_schema.system_users\n" + "ORDER BY user_name"); } else if (dbProductName.indexOf("Oracle") > -1) { statement = shared.jdbcConn.createStatement(); statement.execute( "SELECT username, created FROM all_users " + "ORDER BY username"); } else if (dbProductName.indexOf("PostgreSQL") > -1) { statement = shared.jdbcConn.createStatement(); statement.execute( "SELECT usename, usesuper FROM pg_catalog.pg_user " + "ORDER BY usename"); } else if (dbProductName.indexOf( "Adaptive Server Enterprise") > -1) { // This is the basic Sybase server. Sybase also has // their "Anywhere", ASA (for embedded), and replication // databases, but I don't know the Metadata strings for // those. statement = shared.jdbcConn.createStatement(); statement.execute( "SELECT name, accdate, fullname FROM syslogins " + "ORDER BY name"); } else if (dbProductName.indexOf( "Apache Derby") > -1) { throw new BadSpecial( SqltoolRB.vendor_derby_du.getString()); } else { throw new BadSpecial( SqltoolRB.vendor_nosup_d.getString("u")); } break; case 'a' : if (dbProductName.indexOf("HSQL") > -1 && (minorVersion < 9 && majorVersion < 2)) { // HSQLDB after 1.8 doesn't support any type of aliases // Earlier HSQLDB Aliases are not the same things as // the aliases listed in DatabaseMetaData.getTables(). if (filter != null) { Matcher matcher = dotPattern.matcher(filter); if (matcher.matches()) { filter = (matcher.group(2).length() > 0) ? matcher.group(2) : null; narrower = "\nWHERE alias_schema = '" + ((matcher.group(1).length() > 0) ? matcher.group(1) : getCurrentSchema()) + "'"; } } statement = shared.jdbcConn.createStatement(); statement.execute( "SELECT alias_schem, alias FROM " + "information_schema.system_aliases" + narrower); } else { types[0] = "ALIAS"; } break; case 't' : excludeSysSchemas = (dbProductName.indexOf("Oracle") > -1); types[0] = "TABLE"; break; case 'v' : types[0] = "VIEW"; break; case 'n' : rs = md.getSchemas(); if (rs == null) { throw new BadSpecial( "Failed to get metadata from database"); } displayResultSet(null, rs, listMDSchemaCols, filter); return; case 'i' : // Some databases require to specify table, some don't. /* if (filter == null) { throw new BadSpecial("You must specify the index's " + "table as argument to \\di"); } */ String table = null; if (filter != null) { Matcher matcher = dotPattern.matcher(filter); if (matcher.matches()) { table = (matcher.group(2).length() > 0) ? matcher.group(2) : null; schema = (matcher.group(1).length() > 0) ? matcher.group(1) : getCurrentSchema(); } else { table = filter; } filter = null; } // N.b. Oracle incorrectly reports the INDEX SCHEMA as // the TABLE SCHEMA. The Metadata structure seems to // be designed with the assumption that the INDEX schema // will be the same as the TABLE schema. rs = md.getIndexInfo(null, schema, table, false, true); if (rs == null) { throw new BadSpecial( "Failed to get metadata from database"); } displayResultSet(null, rs, listMDIndexCols, null); return; default : throw new BadSpecial(SqltoolRB.special_d_unknown.getString( Character.toString(c)) + LS + D_OPTIONS_TEXT); } if (statement == null) { if (dbProductName.indexOf("HSQL") > -1) { listSet = listMDTableCols[HSQLDB_ELEMENT]; } else if (dbProductName.indexOf("Oracle") > -1) { listSet = listMDTableCols[ORACLE_ELEMENT]; } else { listSet = listMDTableCols[DEFAULT_ELEMENT]; } if (schema == null && filter != null) { Matcher matcher = dotPattern.matcher(filter); if (matcher.matches()) { filter = (matcher.group(2).length() > 0) ? matcher.group(2) : null; schema = (matcher.group(1).length() > 0) ? matcher.group(1) : getCurrentSchema(); } } } rs = ((statement == null) ? md.getTables(null, schema, null, types) : statement.getResultSet()); if (rs == null) { throw new BadSpecial(SqltoolRB.metadata_fetch_fail.getString()); } displayResultSet(null, rs, listSet, filter); if (additionalSchemas != null) { for (String additionalSchema : additionalSchemas) { /* * Inefficient, but we have to do each successful query * twice in order to prevent calling displayResultSet * for empty/non-existent schemas */ rs = md.getTables(null, additionalSchema, null, types); if (rs == null) { throw new BadSpecial( SqltoolRB.metadata_fetch_failfor.getString( additionalSchema)); } if (!rs.next()) { continue; } displayResultSet( null, md.getTables( null, additionalSchema, null, types), listSet, filter); } } } catch (SQLException se) { throw new BadSpecial(SqltoolRB.metadata_fetch_fail.getString(), se); } catch (NullPointerException npe) { throw new BadSpecial(SqltoolRB.metadata_fetch_fail.getString(), npe); } finally { excludeSysSchemas = false; if (rs != null) { try { rs.close(); } catch (SQLException se) { // We already got what we want from it, or have/are // processing a more specific error. } } if (statement != null) { try { statement.close(); } catch (SQLException se) { // Purposefully doing nothing } } } } private boolean excludeSysSchemas; /** * Process the contents of Edit Buffer as an SQL Statement * * @throws SQLException thrown by JDBC driver. * @throws SqlToolError all other errors. */ private void processSQL() throws SQLException, SqlToolError { requireConnection(); if (buffer == null) throw new RuntimeException( "Internal assertion failed. No buffer in processSQL()."); if (buffer.type != Token.SQL_TYPE) throw new RuntimeException( "Internal assertion failed. " + "Token type " + buffer.getTypeString() + " in processSQL()."); // No reason to check autoCommit constantly. If we need to roll // back, we will check the autocommit state at that time. lastSqlStatement = (plMode ? dereference(buffer.val, true) : buffer.val); // N.b. "lastSqlStatement" is a misnomer only inside this method. // Outside of this method, this var references the "last" SQL // statement which we attempted to execute. if ((!permitEmptySqlStatements) && buffer.val == null || buffer.val.trim().length() < 1) { throw new SqlToolError(SqltoolRB.sqlstatement_empty.getString()); // There is nothing inherently wrong with issuing // an empty command, like to test DB server health. // But, this check effectively catches many syntax // errors early. } Statement statement = null; long startTime = 0; if (reportTimes) startTime = (new java.util.Date()).getTime(); try { // VERY outer block just to ensure we close "statement" try { if (doPrepare) { if (lastSqlStatement.indexOf('?') < 1) { lastSqlStatement = null; throw new SqlToolError(SqltoolRB.prepare_demandqm.getString()); } doPrepare = false; PreparedStatement ps = shared.jdbcConn.prepareStatement(lastSqlStatement); statement = ps; if (prepareVar == null) { if (binBuffer == null) { lastSqlStatement = null; throw new SqlToolError( SqltoolRB.binbuffer_empty.getString()); } ps.setBytes(1, binBuffer); } else { String val = shared.userVars.get(prepareVar); if (val == null) { lastSqlStatement = null; throw new SqlToolError( SqltoolRB.plvar_undefined.getString(prepareVar)); } prepareVar = null; ps.setString(1, val); } ps.executeUpdate(); } else { statement = shared.jdbcConn.createStatement(); statement.execute(lastSqlStatement); } } finally { if (reportTimes) { long elapsed = (new java.util.Date().getTime()) - startTime; //condlPrintln("</TABLE>", true); condlPrintln(SqltoolRB.exectime_report.getString( (int) elapsed), false); } } /* This catches about the only very safe way to know a COMMIT * is not needed. */ try { shared.possiblyUncommitteds = !shared.jdbcConn.getAutoCommit() && !commitOccursPattern.matcher(lastSqlStatement).matches(); } catch (java.sql.SQLException se) { // If connection is closed by instance shutdown or whatever, we'll // get here. lastSqlStatement = null; // I forget what this is for try { shared.jdbcConn.close(); } catch (Exception anye) { // Intentionally empty } shared.jdbcConn = null; shared.possiblyUncommitteds = false; stdprintln(SqltoolRB.disconnect_success.getString()); return; } ResultSet rs = null; try { rs = statement.getResultSet(); displayResultSet(statement, rs, null, null); } finally { if (rs != null) { try { rs.close(); } catch (SQLException se) { // We already got what we want from it, or have/are // processing a more specific error. } } } } finally { try { if (statement != null) statement.close(); } catch (SQLException se) { // Purposefully doing nothing } } lastSqlStatement = null; } /** * Display the given result set for user. * The last 3 params are to narrow down records and columns where * that can not be done with a where clause (like in metadata queries). * <P/> * Caller is responsible for closing any passed Statement or ResultSet. * * @param statement The SQL Statement that the result set is for. * (This is so we can get the statement's update count. * Can be null for non-update queries.) * @param r The ResultSet to display. * @param incCols Optional list of which columns to include (i.e., if * given, then other columns will be skipped). * @param filterRegex Optional filter. Rows are skipped which to not * contain this substring in ANY COLUMN. * (Should add another param to specify targeted columns). * @throws SQLException thrown by JDBC driver. * @throws SqlToolError all other errors. */ private void displayResultSet(Statement statement, ResultSet r, int[] incCols, String filterString) throws SQLException, SqlToolError { java.sql.Timestamp ts; int dotAt; int updateCount = (statement == null) ? -1 : statement .getUpdateCount(); boolean silent = silentFetch; boolean binary = fetchBinary; Pattern filter = null; silentFetch = false; fetchBinary = false; if (filterString != null) try { filter = Pattern.compile(filterString); } catch (PatternSyntaxException pse) { throw new SqlToolError( SqltoolRB.regex_malformat.getString(pse.getMessage())); } if (excludeSysSchemas) { stdprintln(SqltoolRB.vendor_nosup_sysschemas.getString()); } switch (updateCount) { case -1 : if (r == null) { stdprintln(SqltoolRB.noresult.getString(), true); break; } ResultSetMetaData m = r.getMetaData(); int cols = m.getColumnCount(); int incCount = (incCols == null) ? cols : incCols .length; String val; List<String[]> rows = new ArrayList<String[]>(); String[] headerArray = null; String[] fieldArray; int[] maxWidth = new int[incCount]; int insi; boolean skip; boolean isValNull; // STEP 1: GATHER DATA if (!htmlMode) { for (int i = 0; i < maxWidth.length; i++) { maxWidth[i] = 0; } } boolean[] rightJust = new boolean[incCount]; int[] dataType = new int[incCount]; boolean[] autonulls = new boolean[incCount]; insi = -1; headerArray = new String[incCount]; for (int i = 1; i <= cols; i++) { if (incCols != null) { skip = true; for (int j = 0; j < incCols.length; j++) { if (i == incCols[j]) { skip = false; } } if (skip) { continue; } } headerArray[++insi] = m.getColumnLabel(i); dataType[insi] = m.getColumnType(i); rightJust[insi] = false; autonulls[insi] = true; // This is what we want for java.sql.Types.ARRAY : switch (dataType[insi]) { case java.sql.Types.BIGINT : case java.sql.Types.BIT : case java.sql.Types.DECIMAL : case java.sql.Types.DOUBLE : case java.sql.Types.FLOAT : case java.sql.Types.INTEGER : case java.sql.Types.NUMERIC : case java.sql.Types.REAL : case java.sql.Types.SMALLINT : case java.sql.Types.TINYINT : rightJust[insi] = true; break; case java.sql.Types.VARBINARY : case java.sql.Types.VARCHAR : case java.sql.Types.BLOB : case java.sql.Types.CLOB : case java.sql.Types.LONGVARBINARY : case java.sql.Types.LONGVARCHAR : autonulls[insi] = false; break; } if (htmlMode) { continue; } if (headerArray[insi] != null && headerArray[insi].length() > maxWidth[insi]) { maxWidth[insi] = headerArray[insi].length(); } } boolean filteredOut; while (r.next()) { fieldArray = new String[incCount]; insi = -1; filteredOut = filter != null; for (int i = 1; i <= cols; i++) { // This is the only case where we can save a data // read by recognizing we don't need this datum early. if (incCols != null) { skip = true; for (int incCol : incCols) { if (i == incCol) { skip = false; } } if (skip) { continue; } } // This row may still be ditched, but it is now // certain that we need to increment the fieldArray // index. ++insi; if (!SqlFile.canDisplayType(dataType[insi])) { binary = true; } val = null; isValNull = true; if (!binary) { /* * The special formatting for all time-related * fields is because the most popular current * databases are extremely inconsistent about * what resolution is returned for the same types. * In my experience so far, Dates MAY have * resolution down to second, but only TIMESTAMPs * support sub-second res. (and always can). * On top of that there is no consistency across * getObject().toString(). Oracle doesn't even * implement it for their custom TIMESTAMP type. */ switch (dataType[insi]) { case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE: case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE: case java.sql.Types.TIMESTAMP: case java.sql.Types.DATE: case java.sql.Types.TIME: ts = r.getTimestamp(i); isValNull = r.wasNull(); val = ((ts == null) ? null : ts.toString()); // Following block truncates non-zero // sub-seconds from time types OTHER than // TIMESTAMP. if (dataType[insi] != java.sql.Types.TIMESTAMP && dataType[insi] != org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE && val != null) { dotAt = val.lastIndexOf('.'); for (int z = dotAt + 1; z < val.length(); z++) { if (val.charAt(z) != '0') { dotAt = 0; break; } } if (dotAt > 1) { val = val.substring(0, dotAt); } } break; default: val = r.getString(i); isValNull = r.wasNull(); // If we tried to get a String but it // failed, try getting it with a String // Stream if (val == null) { try { val = streamToString( r.getAsciiStream(i), shared.encoding); isValNull = r.wasNull(); } catch (Exception e) { // This isn't an error. // We are attempting to do a stream // fetch if-and-only-if the column // supports it. } } } } if (binary || (val == null &&!isValNull)) { if (pwDsv != null) { throw new SqlToolError( SqltoolRB.dsv_bincol.getString()); } // DB has a value but we either explicitly want // it as binary, or we failed to get it as String. try { binBuffer = SqlFile.streamToBytes(r.getBinaryStream(i)); isValNull = r.wasNull(); } catch (IOException ioe) { throw new SqlToolError( "Failed to read value using stream", ioe); } stdprintln(SqltoolRB.binbuf_write.getString( Integer.toString(binBuffer.length), headerArray[insi], SqlFile.sqlTypeToString(dataType[insi]) )); return; } if (excludeSysSchemas && val != null && i == 2) { for (String oracleSysSchema : oracleSysSchemas) { if (val.equals(oracleSysSchema)) { filteredOut = true; break; } } } shared.userVars.put("?", ((val == null) ? nullRepToken : val)); if (fetchingVar != null) { shared.userVars.put( fetchingVar, shared.userVars.get("?")); updateUserSettings(); fetchingVar = null; } if (silent) { return; } // We do not omit rows here. We collect information // so we can make the decision after all rows are // read in. if (filter != null && (val == null || filter.matcher(val).find())) { filteredOut = false; } /////////////////////////////// // A little tricky here. fieldArray[] MUST get set. if (val == null && pwDsv == null) { if (dataType[insi] == java.sql.Types.VARCHAR) { fieldArray[insi] = (htmlMode ? "<I>null</I>" : nullRepToken); } else { fieldArray[insi] = ""; } } else { fieldArray[insi] = val; } /////////////////////////////// if (htmlMode || pwDsv != null) { continue; } if (fieldArray[insi].length() > maxWidth[insi]) { maxWidth[insi] = fieldArray[insi].length(); } } if (!filteredOut) { rows.add(fieldArray); } } // STEP 2: DISPLAY DATA (= 2a OR 2b) // STEP 2a (Non-DSV) if (pwDsv == null) { condlPrintln("<TABLE border='1'>", true); if (incCount > 1) { condlPrint(SqlFile.htmlRow(COL_HEAD) + LS + PRE_TD, true); for (int i = 0; i < headerArray.length; i++) { condlPrint("<TD>" + headerArray[i] + "</TD>", true); condlPrint(((i > 0) ? " " : "") + ((i < headerArray.length - 1 || rightJust[i]) ? StringUtil.toPaddedString( headerArray[i], maxWidth[i], ' ', !rightJust[i]) : headerArray[i]) , false); } condlPrintln(LS + PRE_TR + "</TR>", true); condlPrintln("", false); if (!htmlMode) { for (int i = 0; i < headerArray.length; i++) { condlPrint(((i > 0) ? " " : "") + SqlFile.divider( maxWidth[i]), false); } condlPrintln("", false); } } for (int i = 0; i < rows.size(); i++) { condlPrint(SqlFile.htmlRow(((i % 2) == 0) ? COL_EVEN : COL_ODD) + LS + PRE_TD, true); fieldArray = rows.get(i); for (int j = 0; j < fieldArray.length; j++) { condlPrint("<TD>" + fieldArray[j] + "</TD>", true); condlPrint(((j > 0) ? " " : "") + ((j < fieldArray.length - 1 || rightJust[j]) ? StringUtil.toPaddedString( fieldArray[j], maxWidth[j], ' ', !rightJust[j]) : fieldArray[j]) , false); } condlPrintln(LS + PRE_TR + "</TR>", true); condlPrintln("", false); } condlPrintln("</TABLE>", true); if (interactive && rows.size() != 1) { stdprintln(LS + SqltoolRB.rows_fetched.getString( rows.size()), true); } condlPrintln("<HR>", true); break; } // STEP 2b (DSV) if (incCount > 0) { for (int i = 0; i < headerArray.length; i++) { dsvSafe(headerArray[i]); pwDsv.print(headerArray[i]); if (i < headerArray.length - 1) { pwDsv.print(dsvColDelim); } } pwDsv.print(dsvRowDelim); } for (String[] fArray : rows) { for (int j = 0; j < fArray.length; j++) { dsvSafe(fArray[j]); pwDsv.print((fArray[j] == null) ? (autonulls[j] ? "" : nullRepToken) : fArray[j]); if (j < fArray.length - 1) { pwDsv.print(dsvColDelim); } } pwDsv.print(dsvRowDelim); } stdprintln(SqltoolRB.rows_fetched_dsv.getString(rows.size())); // Undecided about whether should display row count here when // in non-interactive mode break; default : shared.userVars.put("?", Integer.toString(updateCount)); if (fetchingVar != null) { shared.userVars.put(fetchingVar, shared.userVars.get("?")); updateUserSettings(); fetchingVar = null; } if (updateCount != 0 && interactive) { stdprintln((updateCount == 1) ? SqltoolRB.row_update_singular.getString() : SqltoolRB.row_update_multiple.getString(updateCount)); } break; } } private static final int COL_HEAD = 0, COL_ODD = 1, COL_EVEN = 2 ; private static final String PRE_TR = " "; private static final String PRE_TD = " "; /** * Print a properly formatted HTML <TR> command for the given * situation. * * @param colType Column type: COL_HEAD, COL_ODD or COL_EVEN. */ private static String htmlRow(int colType) { switch (colType) { case COL_HEAD : return PRE_TR + "<TR style='font-weight: bold;'>"; case COL_ODD : return PRE_TR + "<TR style='background: #94d6ef; font: normal " + "normal 10px/10px Arial, Helvitica, sans-serif;'>"; case COL_EVEN : return PRE_TR + "<TR style='background: silver; font: normal " + "normal 10px/10px Arial, Helvitica, sans-serif;'>"; } return null; } /** * Returns a divider of hypens of requested length. * * @param len Length of output String. */ private static String divider(int len) { return (len > DIVIDER.length()) ? DIVIDER : DIVIDER.substring(0, len); } /** * Display command history. */ private void showHistory() throws BadSpecial { if (history == null) { throw new BadSpecial(SqltoolRB.history_unavailable.getString()); } if (history.size() < 1) { throw new BadSpecial(SqltoolRB.history_none.getString()); } if (shared.psStd == null) return; // Input can be dual-purpose, i.e. the script can be intended for // both interactive and non-interactive usage. Token token; for (int i = 0; i < history.size(); i++) { token = history.get(i); shared.psStd.println("#" + (i + oldestHist) + " or " + (i - history.size()) + ':'); shared.psStd.println(token.reconstitute()); } if (buffer != null) { shared.psStd.println(SqltoolRB.editbuffer_contents.getString( buffer.reconstitute())); } shared.psStd.println(); shared.psStd.println(SqltoolRB.buffer_instructions.getString()); } /** * Return a Command from command history. */ private Token commandFromHistory(int inIndex) throws BadSpecial { int index = inIndex; // Just to quiet compiler warnings. if (history == null) { throw new BadSpecial(SqltoolRB.history_unavailable.getString()); } if (index == 0) { throw new BadSpecial(SqltoolRB.history_number_req.getString()); } if (index > 0) { // Positive command# given index -= oldestHist; if (index < 0) { throw new BadSpecial( SqltoolRB.history_backto.getString(oldestHist)); } if (index >= history.size()) { throw new BadSpecial(SqltoolRB.history_upto.getString( history.size() + oldestHist - 1)); } } else { // Negative command# given index += history.size(); if (index < 0) { throw new BadSpecial( SqltoolRB.history_back.getString(history.size())); } } return history.get(index); } /** * Search Command History for a regex match. * * @return Absolute command number, if any match. */ private Integer historySearch(String findRegex) throws BadSpecial { if (history == null) { throw new BadSpecial(SqltoolRB.history_unavailable.getString()); } Pattern pattern = null; try { pattern = Pattern.compile("(?ims)" + findRegex); } catch (PatternSyntaxException pse) { throw new BadSpecial( SqltoolRB.regex_malformat.getString(pse.getMessage())); } // Make matching more liberal. Users can customize search behavior // by using "(?-OPTIONS)" or (?OPTIONS) in their regexes. for (int index = history.size() - 1; index >= 0; index--) if (pattern.matcher((history.get(index)).val).find()) return Integer.valueOf(index + oldestHist); return null; } /** * Set buffer, unless the given token equals what is already in the * buffer. */ private boolean setBuf(Token newBuffer) { if (buffer != null) if (buffer != null && buffer.equals(newBuffer)) return false; switch (newBuffer.type) { case Token.SQL_TYPE: case Token.PL_TYPE: case Token.SPECIAL_TYPE: break; default: throw new RuntimeException( "Internal assertion failed. " + "Attempted to add command type " + newBuffer.getTypeString() + " to buffer"); } buffer = new Token(newBuffer.type, new String(newBuffer.val), newBuffer.line); // System.err.println("Buffer is now (" + buffer + ')'); return true; } int oldestHist = 1; /** * Add a command onto the history list. */ private boolean historize() { if (history == null || buffer == null) { return false; } if (history.size() > 0 && history.get(history.size() - 1).equals(buffer)) { // Don't store two consecutive commands that are exactly the same. return false; } history.add(buffer); if (history.size() <= maxHistoryLength) { return true; } history.remove(0); oldestHist++; return true; } /** * Describe the columns of specified table. * * @param tableName Table that will be described. * @param filter Optional regex to filter by. * By default, will match only against the column name. * Prefix with "/" to match against the entire output line. */ private void describe(String tableName, String filterString) throws SQLException { if (shared.jdbcConn == null) throw new RuntimeException( "Somehow got to 'describe' even though we have no Conn"); /* * Doing case-sensitive filters now, for greater portability. String filter = ((inFilter == null) ? null : inFilter.toUpperCase()); */ Pattern filter = null; boolean filterMatchesAll = false; // match filter against all cols. List<String[]> rows = new ArrayList<String[]>(); String[] headerArray = { SqltoolRB.describe_table_name.getString(), SqltoolRB.describe_table_datatype.getString(), SqltoolRB.describe_table_width.getString(), SqltoolRB.describe_table_nonulls.getString(), }; String[] fieldArray; int[] maxWidth = { 0, 0, 0, 0 }; boolean[] rightJust = { false, false, true, false }; if (filterString != null) try { filterMatchesAll = (filterString.charAt(0) == '/'); filter = Pattern.compile(filterMatchesAll ? filterString.substring(1) : filterString); } catch (PatternSyntaxException pse) { throw new SQLException(SqltoolRB.regex_malformat.getString( pse.getMessage())); // This is obviously not a SQLException. // Perhaps change input parameter to a Pattern to require // caller to compile the pattern? } for (int i = 0; i < headerArray.length; i++) { if (htmlMode) { continue; } if (headerArray[i].length() > maxWidth[i]) { maxWidth[i] = headerArray[i].length(); } } ResultSet r = null; Statement statement = shared.jdbcConn.createStatement(); // STEP 1: GATHER DATA try { statement.execute("SELECT * FROM " + tableName + " WHERE 1 = 2"); r = statement.getResultSet(); ResultSetMetaData m = r.getMetaData(); int cols = m.getColumnCount(); for (int i = 0; i < cols; i++) { fieldArray = new String[4]; fieldArray[0] = m.getColumnName(i + 1); if (filter != null && (!filterMatchesAll) && !filter.matcher(fieldArray[0]).find()) { continue; } fieldArray[1] = m.getColumnTypeName(i + 1); fieldArray[2] = Integer.toString(m.getColumnDisplaySize(i + 1)); fieldArray[3] = ((m.isNullable(i + 1) == java.sql.ResultSetMetaData.columnNullable) ? (htmlMode ? " " : "") : "*"); if (filter != null && filterMatchesAll && !filter.matcher(fieldArray[0] + ' ' + fieldArray[1] + ' ' + fieldArray[2] + ' ' + fieldArray[3]).find()) { continue; } rows.add(fieldArray); for (int j = 0; j < fieldArray.length; j++) { if (fieldArray[j].length() > maxWidth[j]) { maxWidth[j] = fieldArray[j].length(); } } } // STEP 2: DISPLAY DATA condlPrint("<TABLE border='1'>" + LS + SqlFile.htmlRow(COL_HEAD) + LS + PRE_TD, true); for (int i = 0; i < headerArray.length; i++) { condlPrint("<TD>" + headerArray[i] + "</TD>", true); condlPrint(((i > 0) ? " " : "") + ((i < headerArray.length - 1 || rightJust[i]) ? StringUtil.toPaddedString( headerArray[i], maxWidth[i], ' ', !rightJust[i]) : headerArray[i]) , false); } condlPrintln(LS + PRE_TR + "</TR>", true); condlPrintln("", false); if (!htmlMode) { for (int i = 0; i < headerArray.length; i++) { condlPrint(((i > 0) ? " " : "") + SqlFile.divider(maxWidth[i]), false); } condlPrintln("", false); } for (int i = 0; i < rows.size(); i++) { condlPrint(SqlFile.htmlRow(((i % 2) == 0) ? COL_EVEN : COL_ODD) + LS + PRE_TD, true); fieldArray = rows.get(i); for (int j = 0; j < fieldArray.length; j++) { condlPrint("<TD>" + fieldArray[j] + "</TD>", true); condlPrint(((j > 0) ? " " : "") + ((j < fieldArray.length - 1 || rightJust[j]) ? StringUtil.toPaddedString( fieldArray[j], maxWidth[j], ' ', !rightJust[j]) : fieldArray[j]) , false); } condlPrintln(LS + PRE_TR + "</TR>", true); condlPrintln("", false); } condlPrintln(LS + "</TABLE>" + LS + "<HR>", true); } finally { try { if (r != null) { r.close(); } statement.close(); } catch (SQLException se) { // Purposefully doing nothing } } } private boolean eval(String[] inTokens) throws BadSpecial { /* TODO: Rewrite using java.util.regex. */ // dereference *VARNAME variables. // N.b. we work with a "copy" of the tokens. boolean negate = inTokens.length > 0 && inTokens[0].equals("!"); String[] tokens = new String[negate ? (inTokens.length - 1) : inTokens.length]; String inToken; for (int i = 0; i < tokens.length; i++) { inToken = inTokens[i + (negate ? 1 : 0)]; if (inToken.length() > 1 && inToken.charAt(0) == '*') { tokens[i] = shared.userVars.get(inToken.substring(1)); } else { tokens[i] = inTokens[i + (negate ? 1 : 0)]; } // Unset variables permitted in expressions as long as use // the short *VARNAME form. if (tokens[i] == null) { tokens[i] = ""; } } if (tokens.length == 1) { return (tokens[0].length() > 0 &&!tokens[0].equals("0")) ^ negate; } if (tokens.length == 3) { if (tokens[1].equals("==")) { return tokens[0].equals(tokens[2]) ^ negate; } if (tokens[1].equals("!=") || tokens[1].equals("<>") || tokens[1].equals("><")) { return (!tokens[0].equals(tokens[2])) ^ negate; } if (tokens[1].equals(">")) { return (tokens[0].length() > tokens[2].length() || ((tokens[0].length() == tokens[2].length()) && tokens[0].compareTo(tokens[2]) > 0)) ^ negate; } if (tokens[1].equals("<")) { return (tokens[2].length() > tokens[0].length() || ((tokens[2].length() == tokens[0].length()) && tokens[2].compareTo(tokens[0]) > 0)) ^ negate; } } throw new BadSpecial(SqltoolRB.logical_unrecognized.getString()); } private void closeQueryOutputStream() { if (pwQuery == null) { return; } try { if (htmlMode) { pwQuery.println("</BODY></HTML>"); pwQuery.flush(); } } finally { try { pwQuery.close(); } finally { pwQuery = null; // Encourage GC of buffers } } } /** * Print to psStd and possibly pwQuery iff current HTML mode matches * supplied printHtml. */ private void condlPrintln(String s, boolean printHtml) { if ((printHtml &&!htmlMode) || (htmlMode &&!printHtml)) { return; } if (shared.psStd != null) shared.psStd.println(s); if (pwQuery != null) { pwQuery.println(s); pwQuery.flush(); } } /** * Print to psStd and possibly pwQuery iff current HTML mode matches * supplied printHtml. */ private void condlPrint(String s, boolean printHtml) { if ((printHtml &&!htmlMode) || (htmlMode &&!printHtml)) { return; } if (shared.psStd != null) shared.psStd.print(s); if (pwQuery != null) { pwQuery.print(s); pwQuery.flush(); } } private String formatNicely(Map<?, ?> map, boolean withValues) { String s; StringBuffer sb = new StringBuffer(); if (withValues) { SqlFile.appendLine(sb, SqltoolRB.pl_list_parens.getString()); } else { SqlFile.appendLine(sb, SqltoolRB.pl_list_lengths.getString()); } for (Map.Entry<Object, Object> entry : new TreeMap<Object, Object>(map).entrySet()) { s = (String) entry.getValue(); SqlFile.appendLine(sb, " " + (String) entry.getKey() + ": " + (withValues ? ("(" + s + ')') : Integer.toString( s.length()))); } return sb.toString(); } /** * Ascii file dump. * * dumpFile must not be null. */ private void dump(String varName, File dumpFile) throws IOException, BadSpecial { String val = shared.userVars.get(varName); if (val == null) { throw new BadSpecial(SqltoolRB.plvar_undefined.getString(varName)); } OutputStreamWriter osw = new OutputStreamWriter( new FileOutputStream(dumpFile), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding); try { osw.write(val); if (val.length() > 0) { char lastChar = val.charAt(val.length() - 1); if (lastChar != '\n' && lastChar != '\r') { osw.write(LS); } } osw.flush(); } finally { try { osw.close(); } catch (IOException ioe) { // Intentionally empty } finally { osw = null; // Encourage GC of buffers } } // Since opened in overwrite mode, since we didn't exception out, // we can be confident that we wrote all the bytest in the file. stdprintln(SqltoolRB.file_wrotechars.getString( Long.toString(dumpFile.length()), dumpFile.toString())); } byte[] binBuffer; /** * Binary file dump * * dumpFile must not be null. */ private void dump(File dumpFile) throws IOException, BadSpecial { if (binBuffer == null) { throw new BadSpecial(SqltoolRB.binbuffer_empty.getString()); } int len = 0; FileOutputStream fos = new FileOutputStream(dumpFile); try { fos.write(binBuffer); len = binBuffer.length; binBuffer = null; fos.flush(); } finally { try { fos.close(); } catch (IOException ioe) { // Intentionally empty } finally { fos = null; // Encourage GC of buffers } } stdprintln(SqltoolRB.file_wrotechars.getString( len, dumpFile.toString())); } /** * As the name says... * This method always closes the input stream. */ public String streamToString(InputStream isIn, String cs) throws IOException { InputStream is = isIn; // Compiler warning, when we can null the ref byte[] ba = null; int bytesread = 0; int retval; try { try { ba = new byte[is.available()]; } catch (RuntimeException re) { throw new IOException(SqltoolRB.read_toobig.getString()); } while (bytesread < ba.length && (retval = is.read( ba, bytesread, ba.length - bytesread)) > 0) { bytesread += retval; } if (bytesread != ba.length) { throw new IOException( SqltoolRB.read_partial.getString(bytesread, ba.length)); } try { return (cs == null) ? (new String(ba)) : (new String(ba, cs)); } catch (UnsupportedEncodingException uee) { throw new IOException( SqltoolRB.encode_fail.getString(uee.getMessage())); } catch (RuntimeException re) { throw new IOException(SqltoolRB.read_convertfail.getString()); } } finally { try { is.close(); } catch (IOException ioe) { // intentionally empty } finally { is = null; // Encourage GC of buffers } } } /** * Ascii file load. */ private void load(String varName, File asciiFile, String cs) throws IOException { String string = streamToString(new FileInputStream(asciiFile), cs); // The streamToString() method ensures that the Stream gets closed shared.userVars.put(varName, string); updateUserSettings(); } /** * As the name says... */ public static byte[] streamToBytes(InputStream is) throws IOException { byte[] xferBuffer = new byte[10240]; byte[] outBytes = null; int i; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { while ((i = is.read(xferBuffer)) > 0) { baos.write(xferBuffer, 0, i); } outBytes = baos.toByteArray(); } finally { baos = null; // Encourage buffer GC } return outBytes; } /** * Binary file load * * @return The bytes which are the content of the fil */ public static byte[] loadBinary(File binFile) throws IOException { byte[] xferBuffer = new byte[10240]; byte[] outBytes = null; ByteArrayOutputStream baos; int i; FileInputStream fis = new FileInputStream(binFile); try { baos = new ByteArrayOutputStream(); while ((i = fis.read(xferBuffer)) > 0) { baos.write(xferBuffer, 0, i); } outBytes = baos.toByteArray(); } finally { try { fis.close(); } catch (IOException ioe) { // intentionally empty } finally { fis = null; // Encourage GC of buffers baos = null; // Encourage GC of buffers } } return outBytes; } /** * This method is used to tell SqlFile whether this Sql Type must * ALWAYS be loaded to the binary buffer without displaying. * <P> * N.b.: If this returns "true" for a type, then the user can never * "see" values for these columns. * Therefore, if a type may-or-may-not-be displayable, better to return * false here and let the user choose. * In general, if there is a toString() operator for this Sql Type * then return false, since the JDBC driver should know how to make the * value displayable. * </P> * * @see <A href="http://java.sun.com/docs/books/tutorial/jdbc/basics/retrieving.html">http://java.sun.com/docs/books/tutorial/jdbc/basics/retrieving.html</A> * The table on this page lists the most common SqlTypes, all of which * must implement toString() * @see java.sql.Types */ public static boolean canDisplayType(int i) { /* I don't now about some of the more obscure types, like REF and * DATALINK */ switch (i) { //case java.sql.Types.BINARY : case java.sql.Types.BLOB : case java.sql.Types.JAVA_OBJECT : //case java.sql.Types.LONGVARBINARY : //case java.sql.Types.LONGVARCHAR : case java.sql.Types.OTHER : case java.sql.Types.STRUCT : //case java.sql.Types.VARBINARY : return false; } return true; } // won't compile with JDK 1.4 without these private static final int JDBC3_BOOLEAN = 16; private static final int JDBC3_DATALINK = 70; /** * Return a String representation of the specified java.sql.Types type. */ public static String sqlTypeToString(int i) { switch (i) { case java.sql.Types.ARRAY : return "ARRAY"; case java.sql.Types.BIGINT : return "BIGINT"; case java.sql.Types.BINARY : return "BINARY"; case java.sql.Types.BIT : return "BIT"; case java.sql.Types.BLOB : return "BLOB"; case JDBC3_BOOLEAN : return "BOOLEAN"; case java.sql.Types.CHAR : return "CHAR"; case java.sql.Types.CLOB : return "CLOB"; case JDBC3_DATALINK : return "DATALINK"; case java.sql.Types.DATE : return "DATE"; case java.sql.Types.DECIMAL : return "DECIMAL"; case java.sql.Types.DISTINCT : return "DISTINCT"; case java.sql.Types.DOUBLE : return "DOUBLE"; case java.sql.Types.FLOAT : return "FLOAT"; case java.sql.Types.INTEGER : return "INTEGER"; case java.sql.Types.JAVA_OBJECT : return "JAVA_OBJECT"; case java.sql.Types.LONGVARBINARY : return "LONGVARBINARY"; case java.sql.Types.LONGVARCHAR : return "LONGVARCHAR"; case java.sql.Types.NULL : return "NULL"; case java.sql.Types.NUMERIC : return "NUMERIC"; case java.sql.Types.OTHER : return "OTHER"; case java.sql.Types.REAL : return "REAL"; case java.sql.Types.REF : return "REF"; case java.sql.Types.SMALLINT : return "SMALLINT"; case java.sql.Types.STRUCT : return "STRUCT"; case java.sql.Types.TIME : return "TIME"; case java.sql.Types.TIMESTAMP : return "TIMESTAMP"; case java.sql.Types.TINYINT : return "TINYINT"; case java.sql.Types.VARBINARY : return "VARBINARY"; case java.sql.Types.VARCHAR : return "VARCHAR"; case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE : return "SQL_TIME_WITH_TIME_ZONE"; case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE : return "SQL_TIMESTAMP_WITH_TIME_ZONE"; } return "Unknown type " + i; } /** * Validate that String is safe to write TO DSV file. * * @throws SqlToolError if validation fails. */ public void dsvSafe(String s) throws SqlToolError { if (pwDsv == null || dsvColDelim == null || dsvRowDelim == null || nullRepToken == null) { throw new RuntimeException( "Assertion failed. \n" + "dsvSafe called when DSV settings are incomplete"); } if (s == null) { return; } if (s.indexOf(dsvColDelim) > 0) { throw new SqlToolError( SqltoolRB.dsv_coldelim_present.getString(dsvColDelim)); } if (s.indexOf(dsvRowDelim) > 0) { throw new SqlToolError( SqltoolRB.dsv_rowdelim_present.getString(dsvRowDelim)); } if (s.trim().equals(nullRepToken)) { // The trim() is to avoid the situation where the contents of a // field "looks like" the null-rep token. throw new SqlToolError( SqltoolRB.dsv_nullrep_present.getString(nullRepToken)); } } /** * Translates user-supplied escapes into the traditionaly corresponding * corresponding binary characters. * * Allowed sequences: * <UL> * <LI>\0\d+ (an octal digit) * <LI>\[0-9]\d* (a decimal digit) * <LI>\[Xx][0-9]{2} (a hex digit) * <LI>\n Newline (Ctrl-J) * <LI>\r Carriage return (Ctrl-M) * <LI>\t Horizontal tab (Ctrl-I) * <LI>\f Form feed (Ctrl-L) * </UL> * * Java 1.4 String methods will make this into a 1 or 2 line task. */ public static String convertEscapes(String inString) { if (inString == null) { return null; } return convertNumericEscapes( convertEscapes(convertEscapes(convertEscapes(convertEscapes( convertEscapes(inString, "\\n", "\n"), "\\r", "\r"), "\\t", "\t"), "\\\\", "\\"), "\\f", "\f") ); } /** * @param string Non-null String to modify. */ private static String convertNumericEscapes(String string) { String workString = string; int i = 0; for (char dig = '0'; dig <= '9'; dig++) { while ((i = workString.indexOf("\\" + dig, i)) > -1 && i < workString.length() - 1) { workString = convertNumericEscape(string, i); } while ((i = workString.indexOf("\\x" + dig, i)) > -1 && i < workString.length() - 1) { workString = convertNumericEscape(string, i); } while ((i = workString.indexOf("\\X" + dig, i)) > -1 && i < workString.length() - 1) { workString = convertNumericEscape(string, i); } } return workString; } /** * @offset Position of the leading \. */ private static String convertNumericEscape(String string, int offset) { int post = -1; int firstDigit = -1; int radix = -1; if (Character.toUpperCase(string.charAt(offset + 1)) == 'X') { firstDigit = offset + 2; radix = 16; post = firstDigit + 2; if (post > string.length()) post = string.length(); } else { firstDigit = offset + 1; radix = (Character.toUpperCase(string.charAt(firstDigit)) == '0') ? 8 : 10; post = firstDigit + 1; while (post < string.length() && Character.isDigit(string.charAt(post))) post++; } return string.substring(0, offset) + ((char) Integer.parseInt(string.substring(firstDigit, post), radix)) + string.substring(post); } /** * @param string Non-null String to modify. */ private static String convertEscapes(String string, String from, String to) { String workString = string; int i = 0; int fromLen = from.length(); while ((i = workString.indexOf(from, i)) > -1 && i < workString.length() - 1) { workString = workString.substring(0, i) + to + workString.substring(i + fromLen); } return workString; } /** * Name is self-explanatory. * * If there is user demand, open file in random access mode so don't * need to load 2 copies of the entire file into memory. * This will be difficult because can't use standard Java language * features to search through a character array for multi-character * substrings. * * @throws SqlToolError Would prefer to throw an internal exception, * but we want this method to have external * visibility. */ public void importDsv(String filePath, String skipPrefix) throws SqlToolError { requireConnection(); /* To make string comparisons, contains() methods, etc. a little * simpler and concise, just switch all column names to lower-case. * This is ok since we acknowledge up front that DSV import/export * assume no special characters or escaping in column names. */ Matcher matcher; byte[] bfr = null; File dsvFile = new File(filePath); SortedMap<String, String> constColMap = null; if (dsvConstCols != null) { // We trim col. names, but not values. Must allow users to // specify values as spaces, empty string, null. constColMap = new TreeMap<String, String>(); for (String constPair : dsvConstCols.split(dsvColSplitter, -1)) { matcher = nameValPairPattern.matcher(constPair); if (!matcher.matches()) { throw new SqlToolError( SqltoolRB.dsv_constcols_nullcol.getString()); } constColMap.put(matcher.group(1).toLowerCase(), ((matcher.groupCount() < 2 || matcher.group(2) == null) ? "" : matcher.group(2))); } } Set<String> skipCols = null; if (dsvSkipCols != null) { skipCols = new HashSet<String>(); for (String skipCol : dsvSkipCols.split(dsvColSplitter, -1)) { skipCols.add(skipCol.trim().toLowerCase()); } } if (!dsvFile.canRead()) { throw new SqlToolError(SqltoolRB.file_readfail.getString( dsvFile.toString())); } try { bfr = new byte[(int) dsvFile.length()]; } catch (RuntimeException re) { throw new SqlToolError(SqltoolRB.read_toobig.getString(), re); } int bytesread = 0; int retval; InputStream is = null; try { is = new FileInputStream(dsvFile); while (bytesread < bfr.length && (retval = is.read(bfr, bytesread, bfr.length - bytesread)) > 0) { bytesread += retval; } } catch (IOException ioe) { throw new SqlToolError(ioe); } finally { if (is != null) try { is.close(); } catch (IOException ioe) { errprintln( SqltoolRB.inputfile_closefail.getString() + ": " + ioe); } finally { is = null; // Encourage GC of buffers } } if (bytesread != bfr.length) { throw new SqlToolError(SqltoolRB.read_partial.getString( bytesread, bfr.length)); } String dateString; String[] lines = null; try { String string = new String(bfr, (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding); lines = string.split(dsvRowSplitter, -1); } catch (UnsupportedEncodingException uee) { throw new SqlToolError(uee); } catch (RuntimeException re) { throw new SqlToolError(SqltoolRB.read_convertfail.getString(), re); } List<String> headerList = new ArrayList<String>(); String tableName = dsvTargetTable; // First read one until we get one header line int lineCount = 0; String trimmedLine = null; boolean switching = false; int headerOffset = 0; // Used to offset read-start of header record String curLine = "dummy"; // Val will be replaced 4 lines down // This is just to quiet compiler warning while (true) { if (lineCount >= lines.length) throw new SqlToolError(SqltoolRB.dsv_header_none.getString()); curLine = lines[lineCount++]; trimmedLine = curLine.trim(); if (trimmedLine.length() < 1 || (skipPrefix != null && trimmedLine.startsWith(skipPrefix))) { continue; } if (trimmedLine.startsWith("targettable=")) { if (tableName == null) { tableName = trimmedLine.substring( "targettable=".length()).trim(); } continue; } if (trimmedLine.equals("headerswitch{")) { if (tableName == null) { throw new SqlToolError( SqltoolRB.dsv_header_noswitchtarg.getString( lineCount)); } switching = true; continue; } if (trimmedLine.equals("}")) { throw new SqlToolError( SqltoolRB.dsv_header_noswitchmatch.getString(lineCount)); } if (!switching) { break; } int colonAt = trimmedLine.indexOf(':'); if (colonAt < 1 || colonAt == trimmedLine.length() - 1) { throw new SqlToolError( SqltoolRB.dsv_header_nonswitched.getString(lineCount)); } String headerName = trimmedLine.substring(0, colonAt).trim(); // Need to be sure here that tableName is not null (in // which case it would be determined later on by the file name). if (headerName.equals("*") || headerName.equalsIgnoreCase(tableName)){ headerOffset = 1 + curLine.indexOf(':'); break; } // Skip non-matched header line } String headerLine = curLine.substring(headerOffset); String colName; String[] cols = headerLine.split(dsvColSplitter, -1); for (String col : cols) { if (col.length() < 1) { throw new SqlToolError(SqltoolRB.dsv_nocolheader.getString( headerList.size() + 1, lineCount)); } colName = col.trim().toLowerCase(); headerList.add( (colName.equals("-") || (skipCols != null && skipCols.remove(colName)) || (constColMap != null && constColMap.containsKey(colName)) ) ? ((String) null) : colName); } if (skipCols != null && skipCols.size() > 0) { throw new SqlToolError(SqltoolRB.dsv_skipcols_missing.getString( skipCols.toString())); } boolean oneCol = false; // At least 1 non-null column for (String header : headerList) { if (header != null) { oneCol = true; break; } } if (oneCol == false) { // Difficult call, but I think in any real-world situation, the // user will want to know if they are inserting records with no // data from their input file. throw new SqlToolError( SqltoolRB.dsv_nocolsleft.getString(dsvSkipCols)); } int inputColHeadCount = headerList.size(); if (constColMap != null) { headerList.addAll(constColMap.keySet()); } String[] headers = headerList.toArray(new String[0]); // headers contains input headers + all constCols, some of these // values may be nulls. if (tableName == null) { tableName = dsvFile.getName(); int i = tableName.lastIndexOf('.'); if (i > 0) { tableName = tableName.substring(0, i); } } StringBuffer tmpSb = new StringBuffer(); List<String> tmpList = new ArrayList<String>(); int skippers = 0; for (String header : headers) { if (header == null) { skippers++; continue; } if (tmpSb.length() > 0) { tmpSb.append(", "); } tmpSb.append(header); tmpList.add(header); } boolean[] autonulls = new boolean[headers.length - skippers]; boolean[] parseDate = new boolean[autonulls.length]; boolean[] parseBool = new boolean[autonulls.length]; char[] readFormat = new char[autonulls.length]; String[] insertFieldName = tmpList.toArray(new String[] {}); // Remember that the headers array has all columns in DSV file, // even skipped columns. // The autonulls array only has columns that we will insert into. StringBuffer sb = new StringBuffer("INSERT INTO " + tableName + " (" + tmpSb + ") VALUES ("); StringBuffer typeQuerySb = new StringBuffer("SELECT " + tmpSb + " FROM " + tableName + " WHERE 1 = 2"); try { ResultSetMetaData rsmd = shared.jdbcConn.createStatement().executeQuery( typeQuerySb.toString()).getMetaData(); if (rsmd.getColumnCount() != autonulls.length) { throw new SqlToolError( SqltoolRB.dsv_metadata_mismatch.getString()); // Don't know if it's possible to get here. // If so, it's probably a SqlTool problem, not a user or // data problem. // Should be researched and either return a user-friendly // message or a RuntimeExceptin. } for (int i = 0; i < autonulls.length; i++) { autonulls[i] = true; parseDate[i] = false; parseBool[i] = false; readFormat[i] = 's'; // regular Strings switch(rsmd.getColumnType(i + 1)) { case java.sql.Types.BIT : autonulls[i] = true; readFormat[i] = 'b'; break; case java.sql.Types.LONGVARBINARY : case java.sql.Types.VARBINARY : case java.sql.Types.BINARY : autonulls[i] = true; readFormat[i] = 'x'; break; case java.sql.Types.BOOLEAN: parseBool[i] = true; break; case java.sql.Types.ARRAY : autonulls[i] = true; readFormat[i] = 'a'; break; case java.sql.Types.VARCHAR : case java.sql.Types.BLOB : case java.sql.Types.CLOB : case java.sql.Types.LONGVARCHAR : autonulls[i] = false; // This means to preserve white space and to insert // "" for "". Otherwise we trim white space and // insert null for \s*. break; case java.sql.Types.DATE: case java.sql.Types.TIME: case java.sql.Types.TIMESTAMP: case org.hsqldb.types.Types.SQL_TIMESTAMP_WITH_TIME_ZONE: case org.hsqldb.types.Types.SQL_TIME_WITH_TIME_ZONE: parseDate[i] = true; } } } catch (SQLException se) { throw new SqlToolError(SqltoolRB.query_metadatafail.getString( typeQuerySb.toString()), se); } for (int i = 0; i < autonulls.length; i++) { if (i > 0) { sb.append(", "); } sb.append('?'); } // Initialize REJECT file(s) int rejectCount = 0; File rejectFile = null; File rejectReportFile = null; PrintWriter rejectWriter = null; PrintWriter rejectReportWriter = null; try { if (dsvRejectFile != null) try { rejectFile = new File(dsvRejectFile); rejectWriter = new PrintWriter( new OutputStreamWriter(new FileOutputStream(rejectFile), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding)); rejectWriter.print(headerLine + dsvRowDelim); } catch (IOException ioe) { throw new SqlToolError(SqltoolRB.dsv_rejectfile_setupfail.getString( dsvRejectFile), ioe); } if (dsvRejectReport != null) try { rejectReportFile = new File(dsvRejectReport); rejectReportWriter = new PrintWriter(new OutputStreamWriter( new FileOutputStream(rejectReportFile), (shared.encoding == null) ? DEFAULT_FILE_ENCODING : shared.encoding)); rejectReportWriter.println(SqltoolRB.rejectreport_top.getString( (new java.util.Date()).toString(), dsvFile.getPath(), ((rejectFile == null) ? SqltoolRB.none.getString() : rejectFile.getPath()), ((rejectFile == null) ? null : rejectFile.getPath()))); } catch (IOException ioe) { throw new SqlToolError( SqltoolRB.dsv_rejectreport_setupfail.getString( dsvRejectReport), ioe); } int recCount = 0; int skipCount = 0; PreparedStatement ps = null; boolean importAborted = false; boolean doResetAutocommit = false; try { doResetAutocommit = dsvRecordsPerCommit > 0 && shared.jdbcConn.getAutoCommit(); if (doResetAutocommit) shared.jdbcConn.setAutoCommit(false); } catch (SQLException se) { throw new SqlToolError( SqltoolRB.rpc_autocommit_failure.getString(), se); } // We're now assured that if dsvRecordsPerCommit is > 0, then // autocommit is off. try { try { ps = shared.jdbcConn.prepareStatement(sb.toString() + ')'); } catch (SQLException se) { throw new SqlToolError(SqltoolRB.insertion_preparefail.getString( sb.toString()), se); } String[] dataVals = new String[autonulls.length]; // Length is number of cols to insert INTO, not nec. # in DSV file. int readColCount; int storeColCount; Matcher arMatcher; String currentFieldName = null; String[] arVals; // Insert data rows 1-row-at-a-time while (lineCount < lines.length) try { try { curLine = lines[lineCount++]; trimmedLine = curLine.trim(); if (trimmedLine.length() < 1) { continue; // Silently skip blank lines } if (skipPrefix != null && trimmedLine.startsWith(skipPrefix)) { skipCount++; continue; } if (switching) { if (trimmedLine.equals("}")) { switching = false; continue; } int colonAt = trimmedLine.indexOf(':'); if (colonAt < 1 || colonAt == trimmedLine.length() - 1) { throw new SqlToolError(SqltoolRB.dsv_header_matchernonhead.getString( lineCount)); } continue; } // Finished using "trimmed" line now. Whitespace is // meaningful hereafter. // Finally we will attempt to add a record! recCount++; // Remember that recCount counts both inserts + rejects readColCount = 0; storeColCount = 0; cols = curLine.split(dsvColSplitter, -1); for (String col : cols) { if (readColCount == inputColHeadCount) { throw new RowError(SqltoolRB.dsv_colcount_mismatch.getString( inputColHeadCount, 1 + readColCount)); } if (headers[readColCount++] != null) { dataVals[storeColCount++] = dsvTrimAll ? col.trim() : col; } } if (readColCount < inputColHeadCount) { throw new RowError(SqltoolRB.dsv_colcount_mismatch.getString( inputColHeadCount, readColCount)); } /* Already checked for readColCount too high in prev. block */ if (constColMap != null) { for (String val : constColMap.values()) { dataVals[storeColCount++] = val; } } if (storeColCount != dataVals.length) { throw new RowError(SqltoolRB.dsv_insertcol_mismatch.getString( dataVals.length, storeColCount)); } for (int i = 0; i < dataVals.length; i++) { currentFieldName = insertFieldName[i]; if (autonulls[i]) dataVals[i] = dataVals[i].trim(); // N.b. WE SPECIFICALLY DO NOT HANDLE TIMES WITHOUT // DATES, LIKE "3:14:00", BECAUSE, WHILE THIS MAY BE // USEFUL AND EFFICIENT, IT IS NOT PORTABLE. //System.err.println("ps.setString(" + i + ", " // + dataVals[i] + ')'); if (parseDate[i]) { if ((dataVals[i].length() < 1 && autonulls[i]) || dataVals[i].equals(nullRepToken)) { ps.setTimestamp(i + 1, null); } else { dateString = (dataVals[i].indexOf(':') > 0) ? dataVals[i] : (dataVals[i] + " 0:00:00"); // BEWARE: This may not work for some foreign // date/time formats. try { ps.setTimestamp(i + 1, java.sql.Timestamp.valueOf(dateString)); } catch (IllegalArgumentException iae) { throw new RowError(SqltoolRB.time_bad.getString( dateString), iae); } } } else if (parseBool[i]) { if ((dataVals[i].length() < 1 && autonulls[i]) || dataVals[i].equals(nullRepToken)) { ps.setNull(i + 1, java.sql.Types.BOOLEAN); } else { try { ps.setBoolean(i + 1, Boolean.parseBoolean(dataVals[i])); // Boolean... is equivalent to Java 4's // Boolean.parseBoolean(). } catch (IllegalArgumentException iae) { throw new RowError(SqltoolRB.boolean_bad.getString( dataVals[i]), iae); } } } else { switch (readFormat[i]) { case 'b': ps.setBytes( i + 1, (dataVals[i].length() < 1) ? null : SqlFile.bitCharsToBytes( dataVals[i])); break; case 'x': ps.setBytes( i + 1, (dataVals[i].length() < 1) ? null : SqlFile.hexCharOctetsToBytes( dataVals[i])); break; case 'a' : if (SqlFile.createArrayOfMethod == null) { throw new SqlToolError( //SqltoolRB.boolean_bad.getString( "SqlTool requires += Java 1.6 at " + "runtime in order to import Array " + "values"); } if (dataVals[i].length() < 1) { ps.setArray(i + 1, null); break; } arMatcher = arrayPattern.matcher(dataVals[i]); if (!arMatcher.matches()) { throw new RowError( //SqltoolRB.boolean_bad.getString( "Malformatted ARRAY value: (" + dataVals[i] + ')'); } arVals = (arMatcher.group(1) == null) ? (new String[0]) : arMatcher.group(1).split("\\s*,\\s*"); // N.b. THIS DOES NOT HANDLE commas WITHIN // Array ELEMENT VALUES. try { ps.setArray(i + 1, (java.sql.Array) SqlFile.createArrayOfMethod.invoke( shared.jdbcConn, "VARCHAR", arVals)); } catch (IllegalAccessException iae) { throw new RuntimeException(iae); } catch (InvocationTargetException ite) { if (ite.getCause() != null && ite.getCause() instanceof AbstractMethodError) { throw new SqlToolError( //SqltoolRB.boolean_bad.getString( "SqlTool binary is not " + "Array-compatible with your " + "runtime JRE. Array imports " + "not possible."); } throw new RuntimeException(ite); } // createArrayOf method is Java-6-specific! break; default: ps.setString( i + 1, (((dataVals[i].length() < 1 && autonulls[i]) || dataVals[i].equals(nullRepToken)) ? null : dataVals[i])); } } currentFieldName = null; } retval = ps.executeUpdate(); if (retval != 1) { throw new RowError( SqltoolRB.inputrec_modified.getString(retval)); } if (dsvRecordsPerCommit > 0 && (recCount - rejectCount) % dsvRecordsPerCommit == 0) { shared.jdbcConn.commit(); shared.possiblyUncommitteds = false; } else { shared.possiblyUncommitteds = true; } } catch (NumberFormatException nfe) { throw new RowError(null, nfe); } catch (SQLException se) { throw new RowError(null, se); } } catch (RowError re) { rejectCount++; if (rejectWriter != null || rejectReportWriter != null) { if (rejectWriter != null) { rejectWriter.print(curLine + dsvRowDelim); } if (rejectReportWriter != null) { genRejectReportRecord(rejectReportWriter, rejectCount, lineCount, currentFieldName, re.getMessage(), re.getCause()); } } else { importAborted = true; throw new SqlToolError( SqltoolRB.dsv_recin_fail.getString( lineCount, currentFieldName) + ((re.getMessage() == null) ? "" : (" " + re.getMessage())), re.getCause()); } } } finally { if (ps != null) try { ps.close(); } catch (SQLException se) { // We already got what we want from it, or have/are // processing a more specific error. } finally { ps = null; // Encourage GC of buffers } try { if (dsvRecordsPerCommit > 0 && (recCount - rejectCount) % dsvRecordsPerCommit != 0) { // To be consistent, if *DSV_RECORDS_PER_COMMIT is set, we // always commit all inserted records. // This little block commits any straggler commits since the // last commit. shared.jdbcConn.commit(); shared.possiblyUncommitteds = false; } if (doResetAutocommit) shared.jdbcConn.setAutoCommit(true); } catch (SQLException se) { throw new SqlToolError( SqltoolRB.rpc_commit_failure.getString(), se); } String summaryString = null; if (recCount > 0) { summaryString = SqltoolRB.dsv_import_summary.getString( ((skipPrefix == null) ? "" : ("'" + skipPrefix + "'-")), Integer.toString(skipCount), Integer.toString(rejectCount), Integer.toString(recCount - rejectCount), (importAborted ? "importAborted" : null)); stdprintln(summaryString); } try { if (recCount > rejectCount && dsvRecordsPerCommit < 1 && !shared.jdbcConn.getAutoCommit()) { stdprintln(SqltoolRB.insertions_notcommitted.getString()); } } catch (SQLException se) { stdprintln(SqltoolRB.autocommit_fetchfail.getString()); stdprintln(SqltoolRB.insertions_notcommitted.getString()); // No reason to throw here. If user attempts to use the // connection for anything significant, we will throw then. } if (rejectWriter != null) { rejectWriter.flush(); } if (rejectReportWriter != null && rejectCount > 0) { rejectReportWriter.println(SqltoolRB.rejectreport_bottom.getString( summaryString, revnum)); rejectReportWriter.flush(); } if (rejectCount == 0) { if (rejectFile != null && rejectFile.exists() && !rejectFile.delete()) errprintln(SqltoolRB.dsv_rejectfile_purgefail.getString( rejectFile.toString())); if (rejectReportFile != null && !rejectReportFile.delete()) errprintln(SqltoolRB.dsv_rejectreport_purgefail.getString( (rejectFile == null) ? null : rejectFile.toString())); // These are trivial errors. } } } finally { if (rejectWriter != null) { try { rejectWriter.close(); } finally { rejectWriter = null; // Encourage GC of buffers } } if (rejectReportWriter != null) { try { rejectReportWriter.close(); } finally { rejectReportWriter = null; // Encourage GC of buffers } } } } protected static void appendLine(StringBuffer sb, String s) { sb.append(s + LS); } /** * Does a poor-man's parse of a MSDOS command line and parses it * into a WIndows cmd.exe invocation to approximate. */ private static String[] genWinArgs(String monolithic) { List<String> list = new ArrayList<String>(); list.add("cmd.exe"); list.add("/y"); list.add("/c"); Matcher m = wincmdPattern.matcher(monolithic); while (m.find()) { for (int i = 1; i <= m.groupCount(); i++) { if (m.group(i) == null) continue; if (m.group(i).length() > 1 && m.group(i).charAt(0) == '"') { list.add(m.group(i).substring(1, m.group(i).length() - 1)); continue; } list.addAll(Arrays.asList(m.group(i).split("\\s+", -1))); } } return list.toArray(new String[] {}); } private void genRejectReportRecord(PrintWriter pw, int rCount, int lCount, String field, String eMsg, Throwable cause) { pw.println(SqltoolRB.rejectreport_row.getString( ((rCount % 2 == 0) ? "even" : "odd") + "row", Integer.toString(rCount), Integer.toString(lCount), ((field == null) ? " " : field), (((eMsg == null) ? "" : eMsg) + ((eMsg == null || cause == null) ? "" : "<HR/>") + ((cause == null) ? "" : ( (cause instanceof SQLException && cause.getMessage() != null) ? cause.getMessage() : cause.toString() ) ) ))); } /** * Parses input into command tokens, but does not perform the commands * (unless you consider parsing blocks of nested commands to be * "performing" a command). * * Throws only if I/O error or EOF encountered before end of entire file * (encountered at any level of recursion). * * Exceptions thrown within this method percolate right up to the * external call (in scanpass), regardless of ContinueOnErr setting. * This is because it's impossible to know when to terminate blocks * if there is a parsing error. * Only a separate SqlFile invocation (incl. \i command) will cause * a seekTokenSource exception to be handled at a level other than * the very top. */ private TokenList seekTokenSource(String nestingCommand) throws BadSpecial, IOException { Token token; TokenList newTS = new TokenList(); Pattern endPattern = Pattern.compile("end\\s+" + nestingCommand); String subNestingCommand; while ((token = scanner.yylex()) != null) { if (token.type == Token.PL_TYPE && endPattern.matcher(token.val).matches()) { return newTS; } subNestingCommand = nestingCommand(token); if (subNestingCommand != null) { token.nestedBlock = seekTokenSource(subNestingCommand); } newTS.add(token); } throw new BadSpecial( SqltoolRB.pl_block_unterminated.getString(nestingCommand)); } /** * We want leading space to be trimmed. * Leading space should probably not be trimmed, but it is trimmed now * (by the Scanner). */ private void processMacro(Token defToken) throws BadSpecial { Matcher matcher; Token macroToken; if (defToken.val.length() < 1) { throw new BadSpecial(SqltoolRB.macro_tip.getString()); } switch (defToken.val.charAt(0)) { case '?': stdprintln(SqltoolRB.macro_help.getString()); break; case '=': String defString = defToken.val; defString = defString.substring(1).trim(); if (defString.length() < 1) { for (Map.Entry<String, Token> entry : shared.macros.entrySet()) { stdprintln(entry.getKey() + " = " + entry.getValue().reconstitute()); } break; } int newType = -1; StringBuffer newVal = new StringBuffer(); matcher = editMacroPattern.matcher(defString); if (matcher.matches()) { if (buffer == null) { stdprintln(nobufferYetString); return; } newVal.append(buffer.val); if (matcher.groupCount() > 1 && matcher.group(2) != null && matcher.group(2).length() > 0) newVal.append(matcher.group(2)); newType = buffer.type; } else { matcher = spMacroPattern.matcher(defString); if (matcher.matches()) { newVal.append(matcher.group(3)); newType = (matcher.group(2).equals("*") ? Token.PL_TYPE : Token.SPECIAL_TYPE); } else { matcher = sqlMacroPattern.matcher(defString); if (!matcher.matches()) throw new BadSpecial( SqltoolRB.macro_malformat.getString()); newVal.append(matcher.group(2)); newType = Token.SQL_TYPE; } } if (newVal.length() < 1) throw new BadSpecial(SqltoolRB.macrodef_empty.getString()); if (newVal.charAt(newVal.length() - 1) == ';') throw new BadSpecial(SqltoolRB.macrodef_semi.getString()); shared.macros.put(matcher.group(1), new Token(newType, newVal, defToken.line)); break; default: matcher = useMacroPattern.matcher(defToken.val); if (!matcher.matches()) throw new BadSpecial(SqltoolRB.macro_malformat.getString()); macroToken = shared.macros.get(matcher.group(1)); if (macroToken == null) throw new BadSpecial(SqltoolRB.macro_undefined.getString( matcher.group(1))); setBuf(macroToken); buffer.line = defToken.line; if (matcher.groupCount() > 1 && matcher.group(2) != null && matcher.group(2).length() > 0) buffer.val += matcher.group(2); preempt = matcher.group(matcher.groupCount()).equals(";"); } } /** * Convert a String to a byte array by interpreting every 2 characters as * an octal byte value. */ public static byte[] hexCharOctetsToBytes(String hexChars) { int chars = hexChars.length(); if (chars != (chars / 2) * 2) { throw new NumberFormatException("Hex character lists contains " + "an odd number of characters: " + chars); } byte[] ba = new byte[chars/2]; int offset = 0; char c; int octet; for (int i = 0; i < chars; i++) { octet = 0; c = hexChars.charAt(i); if (c >= 'a' && c <= 'f') { octet += 10 + c - 'a'; } else if (c >= 'A' && c <= 'F') { octet += 10 + c - 'A'; } else if (c >= '0' && c <= '9') { octet += c - '0'; } else { throw new NumberFormatException( "Non-hex character in input at offset " + i + ": " + c); } octet = octet << 4; c = hexChars.charAt(++i); if (c >= 'a' && c <= 'f') { octet += 10 + c - 'a'; } else if (c >= 'A' && c <= 'F') { octet += 10 + c - 'A'; } else if (c >= '0' && c <= '9') { octet += c - '0'; } else { throw new NumberFormatException( "Non-hex character in input at offset " + i + ": " + c); } ba[offset++] = (byte) octet; } if (ba.length != offset) { throw new RuntimeException( "Internal accounting problem. Expected to fill buffer of " + "size "+ ba.length + ", but wrote only " + offset + " bytes"); } return ba; } /** * Just a stub for now. */ public static byte[] bitCharsToBytes(String hexChars) { if (hexChars == null) throw new NullPointerException(); // To shut up compiler warn throw new NumberFormatException( "Sorry. Bit exporting not supported yet"); } private void requireConnection() throws SqlToolError { if (shared.jdbcConn == null) throw new SqlToolError(SqltoolRB.no_required_conn.getString()); } /** * Returns a String report for the specified JDBC Connection. * * For databases with poor JDBC support, you won't get much detail. */ public static String getBanner(Connection c) { try { DatabaseMetaData md = c.getMetaData(); return (md == null) ? null : SqltoolRB.jdbc_established.getString( md.getDatabaseProductName(), md.getDatabaseProductVersion(), md.getUserName(), (c.isReadOnly() ? "R/O " : "R/W ") + RCData.tiToString( c.getTransactionIsolation())); } catch (SQLException se) { return null; } } private void displayConnBanner() { String msg = (shared.jdbcConn == null) ? SqltoolRB.disconnected_msg.getString() : SqlFile.getBanner(shared.jdbcConn); stdprintln((msg == null) ? SqltoolRB.connected_fallbackmsg.getString() : msg); } }