package org.codehaus.mojo.dbupgrade.sqlexec; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; import java.io.StringReader; import java.sql.Connection; import java.sql.Driver; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.io.IOUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.StringUtils; /* * Copyright 2000-2010 The Apache Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ /** * Executes SQL against a database. Extracted from sql-maven-plugin 1.3 */ public class DefaultSQLExec implements SQLExec { private SQLExecConfig config; private PrintStream outLog = System.out; public DefaultSQLExec( SQLExecConfig config ) { this.config = config; } // //////////////////////////////// Internal properties////////////////////// private int successfulStatements = 0; private int totalStatements = 0; /** * Database connection */ private Connection conn = null; /** * SQL statement */ private Statement statement = null; /** * Add a SQL transaction to execute */ private Transaction createTransaction() { Transaction t = new Transaction(); // transactions.add( t ); return t; } /** * Add sql command to transactions list. * */ private List<Transaction> addCommandToTransactions( List<Transaction> transactions, String sqlCommand ) { Transaction t = createTransaction(); t.addText( sqlCommand ); transactions.add( t ); return transactions; } /** * Add user sql fileset to transation list * */ private List<Transaction> addFileSetToTransactions( List<Transaction> transactions, FileSet fileset ) { String[] includedFiles; if ( fileset != null ) { fileset.scan(); includedFiles = fileset.getIncludedFiles(); } else { includedFiles = new String[0]; } for ( int j = 0; j < includedFiles.length; j++ ) { Transaction t = createTransaction(); t.setSrc( new File( fileset.getBasedir(), includedFiles[j] ) ); transactions.add( t ); } return transactions; } /** * Add user input of srcFiles to transaction list. * * @throws SQLException */ private List<Transaction> addFilesToTransactions( List<Transaction> transactions, File[] files ) throws SQLException { for ( int i = 0; files != null && i < files.length; ++i ) { if ( files[i] != null && !files[i].exists() ) { throw new SQLException( files[i].getPath() + " not found." ); } Transaction t = createTransaction(); t.setSrc( files[i] ); transactions.add( t ); } return transactions; } /** * Sort the transaction list. */ private void sortTransactions( List<Transaction> transactions ) { if ( SQLExecConfig.FILE_SORTING_ASC.equalsIgnoreCase( this.config.getOrderFile() ) ) { Collections.sort( transactions ); } else if ( SQLExecConfig.FILE_SORTING_DSC.equalsIgnoreCase( this.config.getOrderFile() ) ) { Collections.sort( transactions, Collections.reverseOrder() ); } } private void handleWindowsDomainUser( Properties driverProperties ) { if ( "net.sourceforge.jtds.jdbc.Driver".equals( config.getDriver() ) ) { String[] tokens = StringUtils.split( config.getUsername(), "\\" ); if ( tokens != null && tokens.length == 2 ) { driverProperties.put( "user", tokens[1] ); driverProperties.put( "domain", tokens[0] ); } } } private Driver createJDBCDriver() { Driver driverInstance = null; try { Class<?> dc = Class.forName( config.getDriver() ); driverInstance = (Driver) dc.newInstance(); } catch ( ClassNotFoundException e ) { throw new RuntimeException( "Driver class not found: " + config.getDriver(), e ); } catch ( Exception e ) { throw new RuntimeException( "Failure loading driver: " + config.getDriver(), e ); } return driverInstance; } private Connection createConnection( Driver driverInstance, Properties driverProperties ) throws SQLException { Connection connection = null; for ( int i = 0; i < config.getConnectionRetries(); ++i ) { try { connection = driverInstance.connect( config.getUrl(), driverProperties ); if ( connection == null ) { // Driver doesn't understand the URL throw new RuntimeException( "No suitable Driver for " + config.getUrl() ); } break; } catch ( SQLException e ) { if ( i < config.getConnectionRetries() ) { try { Thread.sleep( config.getConnectionRetryDelay() ); } catch ( Exception iex ) { throw new SQLException( "Unable to connect to " + config.getUrl(), iex ); } continue; } throw new SQLException( "Unable to connect to " + config.getUrl(), e ); } } return connection; } /** * parse driverProperties into Properties set * * @return * @throws SQLException */ protected Properties getDriverProperties() { //set as protected scopy for unit test purpose Properties properties = new Properties(); if ( !StringUtils.isEmpty( this.config.getDriverProperties() ) ) { String[] tokens = StringUtils.split( this.config.getDriverProperties(), "," ); for ( int i = 0; i < tokens.length; ++i ) { String[] keyValueTokens = StringUtils.split( tokens[i].trim(), "=" ); if ( keyValueTokens.length != 2 ) { throw new RuntimeException( "Invalid JDBC Driver properties: " + this.config.getDriverProperties() ); } properties.setProperty( keyValueTokens[0], keyValueTokens[1] ); } } return properties; } /** * read in lines and execute them */ private void runStatements( Reader reader, PrintStream out ) throws SQLException, IOException { String line; StringBuffer sql = new StringBuffer(); BufferedReader in = new BufferedReader( reader ); while ( ( line = in.readLine() ) != null ) { if ( !config.isKeepFormat() ) { line = line.trim(); } if ( !config.isKeepFormat() ) { if ( line.startsWith( "#" ) ) { continue; } if ( line.startsWith( "//" ) ) { continue; } if ( line.startsWith( "--" ) ) { continue; } StringTokenizer st = new StringTokenizer( line ); if ( st.hasMoreTokens() ) { String token = st.nextToken(); if ( "REM".equalsIgnoreCase( token ) ) { continue; } } } if ( !config.isKeepFormat() ) { sql.append( " " ).append( line ); } else { sql.append( "\n" ).append( line ); } // SQL defines "--" as a comment to EOL // and in Oracle it may contain a hint // so we cannot just remove it, instead we must end it if ( !config.isKeepFormat() ) { if ( SqlSplitter.containsSqlEnd( line, config.getDelimiter() ) == SqlSplitter.NO_END ) { sql.append( "\n" ); } } DelimiterType delimiterType = this.config.getDelimiterType(); String delimiter = this.config.getDelimiter(); if ( ( delimiterType.equals( DelimiterType.NORMAL ) && SqlSplitter.containsSqlEnd( line, delimiter ) > 0 ) || ( delimiterType.equals( DelimiterType.ROW ) && line.trim().equals( delimiter ) ) ) { execSQL( sql.substring( 0, sql.length() - delimiter.length() ), out ); sql.setLength( 0 ); // clean buffer } } // Catch any statements not followed by ; if ( !sql.toString().equals( "" ) ) { execSQL( sql.toString(), out ); } } /** * Exec the sql statement. */ private void execSQL( String sql, PrintStream out ) throws SQLException { // Check and ignore empty statements if ( "".equals( sql.trim() ) ) { return; } if ( config.isVerbose() ) { out.append( sql ).append( "\n" ); } ResultSet resultSet = null; try { totalStatements++; boolean ret; int updateCount, updateCountTotal = 0; ret = statement.execute( sql ); updateCount = statement.getUpdateCount(); resultSet = statement.getResultSet(); do { if ( !ret ) { if ( updateCount != -1 ) { updateCountTotal += updateCount; } } else { if ( config.isPrintResultSet() ) { printResultSet( resultSet, out ); } } ret = statement.getMoreResults(); if ( ret ) { updateCount = statement.getUpdateCount(); resultSet = statement.getResultSet(); } } while ( ret ); if ( config.isPrintResultSet() ) { StringBuffer line = new StringBuffer(); line.append( updateCountTotal ).append( " rows affected" ); out.println( line ); } SQLWarning warning = conn.getWarnings(); while ( warning != null ) { warning = warning.getNextWarning(); } conn.clearWarnings(); successfulStatements++; } catch ( SQLException e ) { if ( SQLExecConfig.ON_ERROR_ABORT.equalsIgnoreCase( config.getOnError() ) ) { throw new SQLException( "Unable to execute: " + sql, e ); } } finally { if ( resultSet != null ) { resultSet.close(); } } } /** * print any results in the result set. * * @param rs the resultset to print information about * @param out the place to print results * @throws SQLException on SQL problems. */ private void printResultSet( ResultSet rs, PrintStream out ) throws SQLException { if ( rs != null ) { ResultSetMetaData md = rs.getMetaData(); int columnCount = md.getColumnCount(); StringBuffer line = new StringBuffer(); if ( config.isShowheaders() ) { for ( int col = 1; col < columnCount; col++ ) { line.append( md.getColumnName( col ) ); line.append( "," ); } line.append( md.getColumnName( columnCount ) ); out.println( line ); line = new StringBuffer(); } while ( rs.next() ) { boolean first = true; for ( int col = 1; col <= columnCount; col++ ) { String columnValue = rs.getString( col ); if ( columnValue != null ) { columnValue = columnValue.trim(); } if ( first ) { first = false; } else { line.append( "," ); } line.append( columnValue ); } out.println( line ); line = new StringBuffer(); } } out.println(); } /** * Contains the definition of a new transaction element. Transactions allow several files or * blocks of statements to be executed using the same JDBC connection and commit operation in * between. */ private class Transaction implements Comparable<Object> { private File tSrcFile = null; private String tSqlCommand = ""; /** * */ public void setSrc( File src ) { this.tSrcFile = src; } /** * */ public void addText( String sql ) { if ( !StringUtils.isBlank( sql ) ) { this.tSqlCommand += sql; } } /** * */ private void runTransaction( PrintStream out ) throws IOException, SQLException { if ( tSqlCommand.length() != 0 ) { runStatements( new StringReader( tSqlCommand ), out ); } if ( tSrcFile != null ) { Reader reader = null; if ( StringUtils.isEmpty( config.getEncoding() ) ) { reader = new FileReader( tSrcFile ); } else { reader = new InputStreamReader( new FileInputStream( tSrcFile ), config.getEncoding() ); } try { runStatements( reader, out ); } finally { reader.close(); } } } public int compareTo( Object object ) { Transaction transaction = (Transaction) object; if ( transaction.tSrcFile == null ) { if ( this.tSrcFile == null ) { return 0; } else { return Integer.MAX_VALUE; } } else { if ( this.tSrcFile == null ) { return Integer.MIN_VALUE; } else { return this.tSrcFile.compareTo( transaction.tSrcFile ); } } } } // //////////////////////////////////////////////////////////////////////////////////////////// /** * Load the sql file and then it */ private void execute( Connection conn, List<Transaction> transactions ) throws SQLException { try { statement = conn.createStatement(); statement.setEscapeProcessing( config.isEscapeProcessing() ); PrintStream out = System.out; try { if ( config.getOutputFile() != null ) { out = new PrintStream( new BufferedOutputStream( new FileOutputStream( config.getOutputFile() .getAbsolutePath(), config.isAppend() ) ) ); } // Process all transactions for ( Transaction t : transactions ) { t.runTransaction( out ); if ( !config.isAutocommit() ) { conn.commit(); } } } finally { if ( out != null && out != System.out ) { out.close(); } } } catch ( IOException e ) { throw new SQLException( e.getMessage(), e ); } catch ( SQLException e ) { if ( !config.isAutocommit() && conn != null && SQLExecConfig.ON_ERROR_ABORT.equalsIgnoreCase( config.getOnError() ) ) { this.rollbackQuietly(); } throw new SQLException( e.getMessage(), e ); } finally { DbUtils.closeQuietly( statement ); } if ( SQLExecConfig.ON_ERROR_ABORT_AFTER.equalsIgnoreCase( config.getOnError() ) && totalStatements != successfulStatements ) { throw new SQLException( "Some SQL statements failed to execute" ); } } /////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// /** * Creates a new Connection as using the driver, url, userid and password specified. * * The calling method is responsible for closing the connection. * * @return Connection the newly created connection. * @throws SQLException if the UserId/Password/Url is not set or there is no suitable driver * or the driver fails to load. * @throws SQLException if there is problem getting connection with valid url * */ public Connection getConnection() throws SQLException { if ( conn != null ) { return conn; } Properties driverProperties = new Properties(); driverProperties.put( "user", config.getUsername() ); handleWindowsDomainUser( driverProperties ); if ( !config.isEnableAnonymousPassword() ) { if ( !StringUtils.isBlank( this.config.getPassword() ) ) { driverProperties.put( "password", this.config.getPassword() ); } } driverProperties.putAll( this.getDriverProperties() ); Driver driverInstance = this.createJDBCDriver(); conn = this.createConnection( driverInstance, driverProperties ); conn.setAutoCommit( config.isAutocommit() ); return conn; } public void execute( String sqlCommand ) throws SQLException { execute( sqlCommand, new File[0], null ); } public void execute( File[] srcFiles ) throws SQLException { execute( null, srcFiles, null ); } public void execute( FileSet fileset ) throws SQLException { execute( null, null, fileset ); } public void execute( String sqlCommand, File[] srcFiles, FileSet fileset ) throws SQLException { List<Transaction> transactions = new ArrayList<Transaction>(); successfulStatements = 0; totalStatements = 0; if ( !StringUtils.isBlank( sqlCommand ) ) { addCommandToTransactions( transactions, sqlCommand ); } if ( srcFiles != null ) { addFilesToTransactions( transactions, srcFiles ); } if ( fileset != null ) { addFileSetToTransactions( transactions, fileset ); } sortTransactions( transactions ); for ( int i = 0; i < config.getRepeats() / config.getTransactionsPerConnection(); ++i ) { Connection connection = null; try { connection = getConnection(); } catch ( SQLException e ) { if ( !config.isSkipOnConnectionError() ) { throw new SQLException( e.getMessage(), e ); } else { // error on get connection and user asked to skip the rest break; } } for ( int j = 0; j < config.getTransactionsPerConnection(); ++j ) { try { this.execute( connection, transactions ); Thread.sleep( this.config.getSleepTimeBetweenRepeats() ); } catch ( InterruptedException e ) { } } } } public void execute( Reader reader ) throws SQLException { try { statement = this.getConnection().createStatement(); statement.setEscapeProcessing( config.isEscapeProcessing() ); this.runStatements( reader, outLog ); } catch ( IOException e ) { throw new SQLException( "Error reading SQL stream: " + e.getMessage(), e ); } finally { DbUtils.closeQuietly( statement ); statement = null; } } public void execute( File sqlFile ) throws SQLException { Reader reader = null; try { reader = new FileReader( sqlFile ); execute( reader ); } catch ( IOException e ) { throw new SQLException( "Error reading SQL stream: " + e.getMessage(), e ); } finally { IOUtil.close( reader ); } } public void execute( File sqlFile, boolean disableSQLParser ) throws SQLException, IOException { if ( ! disableSQLParser ) { this.execute( sqlFile ); } else { InputStream is = null; Statement statement = null; String sql = null; try { is = new FileInputStream( sqlFile ); sql = IOUtils.toString( is ); statement = getConnection().createStatement(); statement.setEscapeProcessing( false ); if ( statement.execute( sql ) ) { //we expect a false return since the execution has no result set throw new SQLException( "Unable execute SQL Statement:" + sql ); } } finally { DbUtils.closeQuietly( statement ); IOUtils.closeQuietly( is ); } } } public void commit() throws SQLException { if ( ! this.config.isAutocommit() ) { this.getConnection().commit(); } } public void rollback() { try { if ( ! this.config.isAutocommit() ) { this.getConnection().rollback(); } } catch ( SQLException e ) { //unexpected exception, throw runtime to get more attention throw new RuntimeException( "Unable to rollback." ); } } public void rollbackQuietly() { try { this.getConnection().rollback(); } catch ( SQLException e ) { } } public void close() { DbUtils.closeQuietly( this.conn ); this.conn = null; } public void execute( InputStream istream ) throws SQLException { Reader reader = new InputStreamReader( istream ); this.execute( reader ); } /** * Number of SQL statements executed so far that caused errors. * * @return the number */ public int getSuccessfulStatements() { return successfulStatements; } /** * Number of SQL statements executed so far, including the ones that caused errors. * * @return the number */ public int getTotalStatements() { return totalStatements; } }