@ -18,73 +18,119 @@ package org.springframework.jdbc.datasource.init;
import java.io.IOException ;
import java.io.IOException ;
import java.io.LineNumberReader ;
import java.io.LineNumberReader ;
import java.sql.Connection ;
import java.sql.SQLException ;
import java.sql.SQLException ;
import java.sql.Statement ;
import java.util.LinkedList ;
import java.util.LinkedList ;
import java.util.List ;
import java.util.List ;
import org.apache.commons.logging.Log ;
import org.apache.commons.logging.Log ;
import org.apache.commons.logging.LogFactory ;
import org.apache.commons.logging.LogFactory ;
import org.springframework.core.io.support.EncodedResource ;
import org.springframework.core.io.support.EncodedResource ;
import org.springframework.dao.DataAccessException ;
import org.springframework.util.Assert ;
import org.springframework.jdbc.BadSqlGrammarException ;
import org.springframework.util.StringUtils ;
import org.springframework.util.StringUtils ;
/ * *
/ * *
* Generic utility methods for working with SQL scripts . Mainly for
* Generic utility methods for working with SQL scripts . Mainly for internal use
* internal use within the framework .
* within the framework .
*
*
* @author Thomas Risberg
* @author Sam Brannen
* @author Juergen Hoeller
* @author Keith Donald
* @author Dave Syer
* @author Chris Beams
* @author Oliver Gierke
* @author Chris Baldwin
* @author Chris Baldwin
* @since 4 . 0 . 3
* @since 4 . 0 . 3
* /
* /
public abstract class ScriptUtils {
public abstract class ScriptUtils {
private static final Log logger = LogFactory . getLog ( ScriptUtils . class ) ;
/ * *
* Default statement separator within SQL scripts .
* /
public static final String DEFAULT_STATEMENT_SEPARATOR = ";" ;
/ * *
* Default prefix for line comments within SQL scripts .
* /
public static final String DEFAULT_COMMENT_PREFIX = "--" ;
public static final String DEFAULT_COMMENT_PREFIX = "--" ;
public static final char DEFAULT_STATEMENT_SEPARATOR_CHAR = ';' ;
/ * *
* Default start delimiter for block comments within SQL scripts .
public static final String DEFAULT_STATEMENT_SEPARATOR = String . valueOf ( DEFAULT_STATEMENT_SEPARATOR_CHAR ) ;
* /
public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*" ;
public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*" ;
/ * *
* Default end delimiter for block comments within SQL scripts .
* /
public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/" ;
public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/" ;
private static final Log logger = LogFactory . getLog ( ScriptUtils . class ) ;
/ * *
* Prevent instantiation of this utility class .
* /
private ScriptUtils ( ) {
/* no-op */
}
/ * *
/ * *
* Split an SQL script into separate statements delimited by the provided
* Split an SQL script into separate statements delimited by the provided
* delimiter character . Each individual statement will be added to the
* delimiter character . Each individual statement will be added to the
* provided { @code List } .
* provided { @code List } .
* < p > Within a statement , "{@code --}" will be used as the comment prefix ;
* < p > Within the script , { @value # DEFAULT_COMMENT_PREFIX } will be used as the
* any text beginning with the comment prefix and extending to the end of
* comment prefix ; any text beginning with the comment prefix and extending to
* the line will be omitted from the statement . In addition , multiple adjacent
* the end of the line will be omitted from the output . Similarly ,
* whitespace characters will be collapsed into a single space .
* { @value # DEFAULT_BLOCK_COMMENT_START_DELIMITER } and
* { @value # DEFAULT_BLOCK_COMMENT_END_DELIMITER } will be used as the
* < em > start < / em > and < em > end < / em > block comment delimiters : any text enclosed
* in a block comment will be omitted from the output . In addition , multiple
* adjacent whitespace characters will be collapsed into a single space .
* @param script the SQL script
* @param script the SQL script
* @param delim character delimiting each statement & mdash ; typically a ';' character
* @param delimiter character delimiting each statement & mdash ; typically a ';' character
* @param statements the list that will contain the individual statements
* @param statements the list that will contain the individual statements
* @since 4 . 0 . 3
* @see # splitSqlScript ( EncodedResource , String , String , String , String , String , List )
* /
* /
public static void splitSqlScript ( String script , char delim , List < String > statements ) {
public static void splitSqlScript ( String script , char delimiter , List < String > statements ) throws ScriptException {
splitSqlScript ( script , String . valueOf ( delim ) , DEFAULT_COMMENT_PREFIX , DEFAULT_BLOCK_COMMENT_START_DELIMITER ,
splitSqlScript ( null , script , String . valueOf ( delimiter ) , DEFAULT_COMMENT_PREFIX ,
DEFAULT_BLOCK_COMMENT_END_DELIMITER , statements ) ;
DEFAULT_BLOCK_COMMENT_START_DELIMITER , DEFAULT_BLOCK_COMMENT_END_DELIMITER , statements ) ;
}
}
/ * *
/ * *
* Split an SQL script into separate statements delimited by the provided
* Split an SQL script into separate statements delimited by the provided
* delimiter string . Each individual statement will be added to the provided
* delimiter string . Each individual statement will be added to the provided
* { @code List } .
* { @code List } .
* < p > Within a statemen t, the provided { @code commentPrefix } will be honored ;
* < p > Within the scrip t, the provided { @code commentPrefix } will be honored :
* any text beginning with the comment prefix and extending to the end of the
* any text beginning with the comment prefix and extending to the end of the
* line will be omitted from the statement . In addition , multiple adjacent
* line will be omitted from the output . Similarly , the provided
* whitespace characters will be collapsed into a single space .
* { @code blockCommentStartDelimiter } and { @code blockCommentEndDelimiter }
* @param script the SQL script
* delimiters will be honored : any text enclosed in a block comment will be
* @param delim character delimiting each statement & mdash ; typically a ';' character
* omitted from the output . In addition , multiple adjacent whitespace characters
* @param commentPrefix the prefix that identifies line comments in the SQL script & mdash ; typically "--"
* will be collapsed into a single space .
* @param statements the List that will contain the individual statements
* @param resource the resource from which the script was read
* @since 4 . 0 . 3
* @param script the SQL script ; never { @code null } or empty
* @param delimiter text delimiting each statement & mdash ; typically a ';'
* character ; never { @code null }
* @param commentPrefix the prefix that identifies SQL line comments & mdash ;
* typically "--" ; never { @code null } or empty
* @param blockCommentStartDelimiter the < em > start < / em > block comment delimiter ;
* never { @code null } or empty
* @param blockCommentEndDelimiter the < em > end < / em > block comment delimiter ;
* never { @code null } or empty
* @param statements the list that will contain the individual statements
* /
* /
public static void splitSqlScript ( String script , String delim , String commentPrefix , String blockCommentOpen ,
public static void splitSqlScript ( EncodedResource resource , String script , String delimiter , String commentPrefix ,
String blockCommentClose , List < String > statements ) {
String blockCommentStartDelimiter , String blockCommentEndDelimiter , List < String > statements )
throws ScriptException {
Assert . hasText ( script , "script must not be null or empty" ) ;
Assert . notNull ( delimiter , "delimiter must not be null" ) ;
Assert . hasText ( commentPrefix , "commentPrefix must not be null or empty" ) ;
Assert . hasText ( blockCommentStartDelimiter , "blockCommentStartDelimiter must not be null or empty" ) ;
Assert . hasText ( blockCommentEndDelimiter , "blockCommentEndDelimiter must not be null or empty" ) ;
StringBuilder sb = new StringBuilder ( ) ;
StringBuilder sb = new StringBuilder ( ) ;
boolean inLiteral = false ;
boolean inLiteral = false ;
boolean inEscape = false ;
boolean inEscape = false ;
@ -106,13 +152,13 @@ public abstract class ScriptUtils {
inLiteral = ! inLiteral ;
inLiteral = ! inLiteral ;
}
}
if ( ! inLiteral ) {
if ( ! inLiteral ) {
if ( script . startsWith ( delim , i ) ) {
if ( script . startsWith ( delimiter , i ) ) {
// we've reached the end of the current statement
// we've reached the end of the current statement
if ( sb . length ( ) > 0 ) {
if ( sb . length ( ) > 0 ) {
statements . add ( sb . toString ( ) ) ;
statements . add ( sb . toString ( ) ) ;
sb = new StringBuilder ( ) ;
sb = new StringBuilder ( ) ;
}
}
i + = delim . length ( ) - 1 ;
i + = delimiter . length ( ) - 1 ;
continue ;
continue ;
}
}
else if ( script . startsWith ( commentPrefix , i ) ) {
else if ( script . startsWith ( commentPrefix , i ) ) {
@ -128,16 +174,16 @@ public abstract class ScriptUtils {
break ;
break ;
}
}
}
}
else if ( script . startsWith ( blockCommentOpen , i ) ) {
else if ( script . startsWith ( blockCommentStartDelimiter , i ) ) {
// skip over any block comments
// skip over any block comments
int indexOfCommentClose = script . indexOf ( blockCommentClose , i ) ;
int indexOfCommentEnd = script . indexOf ( blockCommentEndDelimiter , i ) ;
if ( indexOfCommentClose > i ) {
if ( indexOfCommentEnd > i ) {
i = indexOfCommentClose + blockCommentClose . length ( ) - 1 ;
i = indexOfCommentEnd + blockCommentEndDelimiter . length ( ) - 1 ;
continue ;
continue ;
}
}
else {
else {
throw new BadSqlGrammarException ( "" , script . substring ( i ) ,
throw new ScriptParseException ( String . format ( "Missing block comment end delimiter [%s]." ,
new SQLException ( "Missing block comment end delimiter" ) ) ;
blockCommentEndDelimiter ) , resource ) ;
}
}
}
}
else if ( c = = ' ' | | c = = '\n' | | c = = '\t' ) {
else if ( c = = ' ' | | c = = '\n' | | c = = '\t' ) {
@ -158,52 +204,32 @@ public abstract class ScriptUtils {
}
}
/ * *
/ * *
* Read a script from the provided { @code LineNumberReader } , using
* Read a script from the given resource , using "{@code --}" as the comment prefix
* "{@code --}" as the comment prefix , and build a { @code String } containing
* and "{@code ;}" as the statement separator , and build a String containing the lines .
* the lines .
* @param lineNumberReader the { @code LineNumberReader } containing the script
* to be processed
* @return a { @code String } containing the script lines
* @see # readScript ( LineNumberReader , String , String )
* @since 4 . 0 . 3
* /
public static String readScript ( LineNumberReader lineNumberReader ) throws IOException {
return readScript ( lineNumberReader , DEFAULT_COMMENT_PREFIX , DEFAULT_STATEMENT_SEPARATOR ) ;
}
/ * *
* Read a script from the given resource , using "{@code --}" as the comment prefix
* and " { @code ; } as the statement separator , and build a String containing the lines .
* @param resource the { @code EncodedResource } to be read
* @param resource the { @code EncodedResource } to be read
* @return { @code String } containing the script lines
* @return { @code String } containing the script lines
* @throws IOException in case of I / O errors
* @throws IOException in case of I / O errors
* @since 4 . 0 . 3
* /
* /
public static String readScript ( EncodedResource resource ) throws IOException {
static String readScript ( EncodedResource resource ) throws IOException {
LineNumberReader lnr = new LineNumberReader ( resource . getReader ( ) ) ;
return readScript ( resource , DEFAULT_COMMENT_PREFIX , DEFAULT_STATEMENT_SEPARATOR ) ;
try {
return readScript ( lnr , DEFAULT_COMMENT_PREFIX , DEFAULT_STATEMENT_SEPARATOR ) ;
}
finally {
lnr . close ( ) ;
}
}
}
/ * *
/ * *
* Read a script from the provided resource , using the supplied
* Read a script from the provided resource , using the supplied
* comment prefix and statement separator , and build a { @code String } containing the lines .
* comment prefix and statement separator , and build a { @code String } containing
* the lines .
* < p > Lines < em > beginning < / em > with the comment prefix are excluded from the
* < p > Lines < em > beginning < / em > with the comment prefix are excluded from the
* results ; however , line comments anywhere else & mdash ; for example , within
* results ; however , line comments anywhere else & mdash ; for example , within
* a statement & mdash ; will be included in the results .
* a statement & mdash ; will be included in the results .
* @param resource the { @code EncodedResource } containing the script
* @param resource the { @code EncodedResource } containing the script
* to be processed
* to be processed
* @param commentPrefix the prefix that identifies comments in the SQL script & mdash ; typically "--"
* @param commentPrefix the prefix that identifies comments in the SQL script & mdash ;
* typically "--"
* @param separator the statement separator in the SQL script & mdash ; typically ";"
* @param separator the statement separator in the SQL script & mdash ; typically ";"
* @return a { @code String } containing the script lines
* @return a { @code String } containing the script lines
* @since 4 . 0 . 3
* /
* /
public static String readScript ( EncodedResource resource , String commentPrefix ,
private static String readScript ( EncodedResource resource , String commentPrefix , String separator )
String separator ) throws IOException {
throws IOException {
LineNumberReader lnr = new LineNumberReader ( resource . getReader ( ) ) ;
LineNumberReader lnr = new LineNumberReader ( resource . getReader ( ) ) ;
try {
try {
return readScript ( lnr , commentPrefix , separator ) ;
return readScript ( lnr , commentPrefix , separator ) ;
@ -212,27 +238,28 @@ public abstract class ScriptUtils {
lnr . close ( ) ;
lnr . close ( ) ;
}
}
}
}
/ * *
/ * *
* Read a script from the provided { @code LineNumberReader } , using the supplied
* Read a script from the provided { @code LineNumberReader } , using the supplied
* comment prefix and statement separator , and build a { @code String } containing the lines .
* comment prefix and statement separator , and build a { @code String } containing
* the lines .
* < p > Lines < em > beginning < / em > with the comment prefix are excluded from the
* < p > Lines < em > beginning < / em > with the comment prefix are excluded from the
* results ; however , line comments anywhere else & mdash ; for example , within
* results ; however , line comments anywhere else & mdash ; for example , within
* a statement & mdash ; will be included in the results .
* a statement & mdash ; will be included in the results .
* @param lineNumberReader the { @code LineNumberReader } containing the script
* @param lineNumberReader the { @code LineNumberReader } containing the script
* to be processed
* to be processed
* @param commentPrefix the prefix that identifies comments in the SQL script & mdash ; typically "--"
* @param commentPrefix the prefix that identifies comments in the SQL script & mdash ;
* typically "--"
* @param separator the statement separator in the SQL script & mdash ; typically ";"
* @param separator the statement separator in the SQL script & mdash ; typically ";"
* @return a { @code String } containing the script lines
* @return a { @code String } containing the script lines
* @since 4 . 0 . 3
* /
* /
public static String readScript ( LineNumberReader lineNumberReader , String commentPrefix ,
public static String readScript ( LineNumberReader lineNumberReader , String commentPrefix , String separator )
String separator ) throws IOException {
throws IOException {
String currentStatement = lineNumberReader . readLine ( ) ;
String currentStatement = lineNumberReader . readLine ( ) ;
StringBuilder scriptBuilder = new StringBuilder ( ) ;
StringBuilder scriptBuilder = new StringBuilder ( ) ;
while ( currentStatement ! = null ) {
while ( currentStatement ! = null ) {
if ( StringUtils . hasText ( currentStatement ) & &
if ( StringUtils . hasText ( currentStatement )
( commentPrefix ! = null & & ! currentStatement . startsWith ( commentPrefix ) ) ) {
& & ( commentPrefix ! = null & & ! currentStatement . startsWith ( commentPrefix ) ) ) {
if ( scriptBuilder . length ( ) > 0 ) {
if ( scriptBuilder . length ( ) > 0 ) {
scriptBuilder . append ( '\n' ) ;
scriptBuilder . append ( '\n' ) ;
}
}
@ -259,21 +286,10 @@ public abstract class ScriptUtils {
}
}
}
}
/ * *
* Does the provided SQL script contain the specified delimiter ?
* @param script the SQL script
* @param delim character delimiting each statement - typically a ';' character
* @since 4 . 0 . 3
* /
public static boolean containsSqlScriptDelimiters ( String script , char delim ) {
return containsSqlScriptDelimiters ( script , String . valueOf ( delim ) ) ;
}
/ * *
/ * *
* Does the provided SQL script contain the specified delimiter ?
* Does the provided SQL script contain the specified delimiter ?
* @param script the SQL script
* @param script the SQL script
* @param delim String delimiting each statement - typically a ';' character
* @param delim String delimiting each statement - typically a ';' character
* @since 4 . 0 . 3
* /
* /
public static boolean containsSqlScriptDelimiters ( String script , String delim ) {
public static boolean containsSqlScriptDelimiters ( String script , String delim ) {
boolean inLiteral = false ;
boolean inLiteral = false ;
@ -291,24 +307,29 @@ public abstract class ScriptUtils {
/ * *
/ * *
* Execute the given SQL script .
* Execute the given SQL script .
* < p > The script will normally be loaded by classpath . There should be one statement
* < p > Statement separators and comments will be removed before executing
* per line . Any statement separators will be removed .
* individual statements within the supplied script .
* < p > < b > Do not use this method to execute DDL if you expect rollback . < / b >
* < p > < b > Do not use this method to execute DDL if you expect rollback . < / b >
* @param executor the { @code ScriptStatementExecutor } with which to perform JDBC operations
* @param connection the JDBC connection to use to execute the script ; already
* @param resource the resource ( potentially associated with a specific encoding ) to load the SQL script from
* configured and ready to use
* @param continueOnError whether or not to continue without throwing an exception in the event of an error
* @param resource the resource ( potentially associated with a specific encoding )
* @param ignoreFailedDrops whether of not to continue in the event of specifically an error on a { @code DROP }
* to load the SQL script from
* @param commentPrefix the script line comment prefix
* @param continueOnError whether or not to continue without throwing an exception
* if not specified
* in the event of an error
* @param separator the script statement separator , defaults to { @code DEFAUT_STATEMENT_SEPARATOR }
* @param ignoreFailedDrops whether or not to continue in the event of specifically
* if not specified
* an error on a { @code DROP } statement
* @param blockCommentStartDelim the script block comment starting delimiter
* @param commentPrefix the prefix that identifies comments in the SQL script & mdash ;
* @param blockCommentEndDelim the script block comment ending delimiter
* typically "--"
* @since 4 . 0 . 3
* @param separator the script statement separator ; defaults to
* { @value # DEFAULT_STATEMENT_SEPARATOR } if not specified
* @param blockCommentStartDelimiter the < em > start < / em > block comment delimiter ; never
* { @code null } or empty
* @param blockCommentEndDelimiter the < em > end < / em > block comment delimiter ; never
* { @code null } or empty
* /
* /
public static void executeSqlScript ( ScriptStatementExecutor executor , EncodedResource resource ,
public static void executeSqlScript ( Connection connection , EncodedResource resource , boolean continueOnError ,
boolean continueOnError , boolean ignoreFailedDrops , String commentPrefix , String separator ,
boolean ignoreFailedDrops , String commentPrefix , String separator , String blockCommentStartDelimite r ,
String blockCommentStartDelim , String blockCommentEndDelim ) throws DataAccessException {
String blockCommentEndDelimiter ) throws SQLException , Script Exception {
if ( logger . isInfoEnabled ( ) ) {
if ( logger . isInfoEnabled ( ) ) {
logger . info ( "Executing SQL script from " + resource ) ;
logger . info ( "Executing SQL script from " + resource ) ;
@ -322,54 +343,55 @@ public abstract class ScriptUtils {
catch ( IOException ex ) {
catch ( IOException ex ) {
throw new CannotReadScriptException ( resource , ex ) ;
throw new CannotReadScriptException ( resource , ex ) ;
}
}
if ( separator = = null ) {
if ( separator = = null ) {
separator = DEFAULT_STATEMENT_SEPARATOR ;
separator = DEFAULT_STATEMENT_SEPARATOR ;
if ( ! containsSqlScriptDelimiters ( script , separator ) ) {
if ( ! containsSqlScriptDelimiters ( script , separator ) ) {
separator = "\n" ;
separator = "\n" ;
}
}
}
}
splitSqlScript ( script , separator , commentPrefix , blockCommentStartDelim , blockCommentEndDelim , statements ) ;
splitSqlScript ( resource , script , separator , commentPrefix , blockCommentStartDelimiter ,
blockCommentEndDelimiter , statements ) ;
int lineNumber = 0 ;
int lineNumber = 0 ;
for ( String statement : statements ) {
Statement stmt = connection . createStatement ( ) ;
lineNumber + + ;
try {
try {
for ( String statement : statements ) {
int rowsAffected = executor . executeScriptStatement ( statement ) ;
lineNumber + + ;
if ( logger . isDebugEnabled ( ) ) {
try {
logger . debug ( rowsAffected + " returned as updateCount for SQL: " + statement ) ;
stmt . execute ( statement ) ;
}
int rowsAffected = stmt . getUpdateCount ( ) ;
}
catch ( DataAccessException ex ) {
boolean dropStatement = StringUtils . startsWithIgnoreCase ( statement . trim ( ) , "drop" ) ;
if ( continueOnError | | ( dropStatement & & ignoreFailedDrops ) ) {
if ( logger . isDebugEnabled ( ) ) {
if ( logger . isDebugEnabled ( ) ) {
logger . debug ( "Failed to execute SQL script statement at line " + lineNumber +
logger . debug ( rowsAffected + " returned as updateCount for SQL: " + statement ) ;
" of resource " + resource + ": " + statement , ex ) ;
}
}
}
}
else {
catch ( SQLException ex ) {
throw new ScriptStatementFailedException ( statement , lineNumber , resource , ex ) ;
boolean dropStatement = StringUtils . startsWithIgnoreCase ( statement . trim ( ) , "drop" ) ;
if ( continueOnError | | ( dropStatement & & ignoreFailedDrops ) ) {
if ( logger . isDebugEnabled ( ) ) {
logger . debug ( "Failed to execute SQL script statement at line " + lineNumber
+ " of resource " + resource + ": " + statement , ex ) ;
}
}
else {
throw new ScriptStatementFailedException ( statement , lineNumber , resource , ex ) ;
}
}
}
}
}
}
}
finally {
try {
stmt . close ( ) ;
}
catch ( Throwable ex ) {
logger . debug ( "Could not close JDBC Statement" , ex ) ;
}
}
long elapsedTime = System . currentTimeMillis ( ) - startTime ;
long elapsedTime = System . currentTimeMillis ( ) - startTime ;
if ( logger . isInfoEnabled ( ) ) {
if ( logger . isInfoEnabled ( ) ) {
logger . info ( "Done executing SQL script from " + resource + " in " + elapsedTime + " ms." ) ;
logger . info ( "Executed SQL script from " + resource + " in " + elapsedTime + " ms." ) ;
}
}
}
}
/ * *
* Interface to be implemented by an object so that { @code executeScript ( ) } is able to use
* it to execute script statements .
* @since 4 . 0 . 3
* /
public interface ScriptStatementExecutor
{
/ * *
* Execute the given SQL statement and return a count of the number of affected rows .
* @return the number of rows affected by the statement
* @throws DataAccessException if there is a problem during statement execution
* /
public int executeScriptStatement ( String statement ) throws DataAccessException ;
}
}
}