com.firefly.db.init.ScriptUtils Maven / Gradle / Ivy
package com.firefly.db.init;
import java.io.IOException;
import java.io.LineNumberReader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.List;
import com.firefly.utils.Assert;
import com.firefly.utils.StringUtils;
import com.firefly.utils.io.EncodedResource;
import com.firefly.utils.io.Resource;
import com.firefly.utils.log.Log;
import com.firefly.utils.log.LogFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Generic utility methods for working with SQL scripts.
*
*
* Mainly for internal use within the framework.
*
*/
public abstract class ScriptUtils {
/**
* Default statement separator within SQL scripts: {@code ";"}.
*/
public static final String DEFAULT_STATEMENT_SEPARATOR = ";";
/**
* Fallback statement separator within SQL scripts: {@code "\n"}.
*
* Used if neither a custom separator nor the
* {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script.
*/
public static final String FALLBACK_STATEMENT_SEPARATOR = "\n";
/**
* End of file (EOF) SQL statement separator:
* {@code "^^^ END OF SCRIPT ^^^"}.
*
* This value may be supplied as the {@code separator} to
* {@link #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)}
* to denote that an SQL script contains a single statement (potentially
* spanning multiple lines) with no explicit statement separator. Note that
* such a script should not actually contain this value; it is merely a
* virtual statement separator.
*/
public static final String EOF_STATEMENT_SEPARATOR = "^^^ END OF SCRIPT ^^^";
/**
* Default prefix for single-line comments within SQL scripts: {@code "--"}.
*/
public static final String DEFAULT_COMMENT_PREFIX = "--";
/**
* Default start delimiter for block comments within SQL scripts:
* {@code "/*"}.
*/
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 = "*/";
private static final Logger logger = LoggerFactory.getLogger("firefly-system");
/**
* Split an SQL script into separate statements delimited by the provided
* separator character. Each individual statement will be added to the
* provided {@code List}.
*
* Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the
* comment prefix; any text beginning with the comment prefix and extending
* to the end of the line will be omitted from the output. Similarly,
* {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and
* {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the
* start and end 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 separator
* character separating each statement — typically a ';'
* @param statements
* the list that will contain the individual statements
* @throws ScriptException
* if an error occurred while splitting the SQL script
* @see #splitSqlScript(String, String, List)
* @see #splitSqlScript(EncodedResource, String, String, String, String,
* String, List)
*/
public static void splitSqlScript(String script, char separator, List statements) throws ScriptException {
splitSqlScript(script, String.valueOf(separator), statements);
}
/**
* Split an SQL script into separate statements delimited by the provided
* separator string. Each individual statement will be added to the provided
* {@code List}.
*
* Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the
* comment prefix; any text beginning with the comment prefix and extending
* to the end of the line will be omitted from the output. Similarly,
* {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and
* {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the
* start and end 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 separator
* text separating each statement — typically a ';' or
* newline character
* @param statements
* the list that will contain the individual statements
* @throws ScriptException
* if an error occurred while splitting the SQL script
* @see #splitSqlScript(String, char, List)
* @see #splitSqlScript(EncodedResource, String, String, String, String,
* String, List)
*/
public static void splitSqlScript(String script, String separator, List statements) throws ScriptException {
splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER,
DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements);
}
/**
* Split an SQL script into separate statements delimited by the provided
* separator string. Each individual statement will be added to the provided
* {@code List}.
*
* Within the script, the provided {@code commentPrefix} will be honored:
* any text beginning with the comment prefix and extending to the end of
* the line will be omitted from the output. Similarly, the provided
* {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter}
* delimiters will be honored: 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 resource
* the resource from which the script was read
* @param script
* the SQL script; never {@code null} or empty
* @param separator
* text separating each statement — typically a ';' or
* newline character; never {@code null}
* @param commentPrefix
* the prefix that identifies SQL line comments — typically
* "--"; never {@code null} or empty
* @param blockCommentStartDelimiter
* the start block comment delimiter; never {@code null}
* or empty
* @param blockCommentEndDelimiter
* the end block comment delimiter; never {@code null}
* or empty
* @param statements
* the list that will contain the individual statements
* @throws ScriptException
* if an error occurred while splitting the SQL script
*/
public static void splitSqlScript(EncodedResource resource, String script, String separator, String commentPrefix,
String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements)
throws ScriptException {
Assert.hasText(script, "script must not be null or empty");
Assert.notNull(separator, "separator 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();
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
boolean inEscape = false;
char[] content = script.toCharArray();
for (int i = 0; i < script.length(); i++) {
char c = content[i];
if (inEscape) {
inEscape = false;
sb.append(c);
continue;
}
// MySQL style escapes
if (c == '\\') {
inEscape = true;
sb.append(c);
continue;
}
if (!inDoubleQuote && (c == '\'')) {
inSingleQuote = !inSingleQuote;
} else if (!inSingleQuote && (c == '"')) {
inDoubleQuote = !inDoubleQuote;
}
if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(separator, i)) {
// we've reached the end of the current statement
if (sb.length() > 0) {
statements.add(sb.toString());
sb = new StringBuilder();
}
i += separator.length() - 1;
continue;
} else if (script.startsWith(commentPrefix, i)) {
// skip over any content from the start of the comment to
// the EOL
int indexOfNextNewline = script.indexOf("\n", i);
if (indexOfNextNewline > i) {
i = indexOfNextNewline;
continue;
} else {
// if there's no EOL, we must be at the end
// of the script, so stop here.
break;
}
} else if (script.startsWith(blockCommentStartDelimiter, i)) {
// skip over any block comments
int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
if (indexOfCommentEnd > i) {
i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
continue;
} else {
throw new ScriptParseException(
String.format("Missing block comment end delimiter [%s].", blockCommentEndDelimiter),
resource);
}
} else if (c == ' ' || c == '\n' || c == '\t') {
// avoid multiple adjacent whitespace characters
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
c = ' ';
} else {
continue;
}
}
}
sb.append(c);
}
if (StringUtils.hasText(sb)) {
statements.add(sb.toString());
}
}
/**
* 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
* @return {@code String} containing the script lines
* @throws IOException
* in case of I/O errors
*/
public static String readScript(EncodedResource resource) throws IOException {
return readScript(resource, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR);
}
/**
* Read a script from the provided resource, using the supplied comment
* prefix and statement separator, and build a {@code String} containing the
* lines.
*
* Lines beginning with the comment prefix are excluded from the
* results; however, line comments anywhere else — for example, within
* a statement — will be included in the results.
*
* @param resource
* the {@code EncodedResource} containing the script to be
* processed
* @param commentPrefix
* the prefix that identifies comments in the SQL script —
* typically "--"
* @param separator
* the statement separator in the SQL script — typically
* ";"
* @return a {@code String} containing the script lines
* @throws IOException
* in case of I/O errors
*/
private static String readScript(EncodedResource resource, String commentPrefix, String separator)
throws IOException {
LineNumberReader lnr = new LineNumberReader(resource.getReader());
try {
return readScript(lnr, commentPrefix, separator);
} finally {
lnr.close();
}
}
/**
* Read a script from the provided {@code LineNumberReader}, using the
* supplied comment prefix and statement separator, and build a
* {@code String} containing the lines.
*
* Lines beginning with the comment prefix are excluded from the
* results; however, line comments anywhere else — for example, within
* a statement — will be included in the results.
*
* @param lineNumberReader
* the {@code LineNumberReader} containing the script to be
* processed
* @param commentPrefix
* the prefix that identifies comments in the SQL script —
* typically "--"
* @param separator
* the statement separator in the SQL script — typically
* ";"
* @return a {@code String} containing the script lines
* @throws IOException
* in case of I/O errors
*/
public static String readScript(LineNumberReader lineNumberReader, String commentPrefix, String separator)
throws IOException {
String currentStatement = lineNumberReader.readLine();
StringBuilder scriptBuilder = new StringBuilder();
while (currentStatement != null) {
if (commentPrefix != null && !currentStatement.startsWith(commentPrefix)) {
if (scriptBuilder.length() > 0) {
scriptBuilder.append('\n');
}
scriptBuilder.append(currentStatement);
}
currentStatement = lineNumberReader.readLine();
}
appendSeparatorToScriptIfNecessary(scriptBuilder, separator);
return scriptBuilder.toString();
}
private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuilder, String separator) {
if (separator == null) {
return;
}
String trimmed = separator.trim();
if (trimmed.length() == separator.length()) {
return;
}
// separator ends in whitespace, so we might want to see if the script
// is trying
// to end the same way
if (scriptBuilder.lastIndexOf(trimmed) == scriptBuilder.length() - trimmed.length()) {
scriptBuilder.append(separator.substring(trimmed.length()));
}
}
/**
* Does the provided SQL script contain the specified delimiter?
*
* @param script
* the SQL script
* @param delim
* String delimiting each statement - typically a ';' character
*/
public static boolean containsSqlScriptDelimiters(String script, String delim) {
boolean inLiteral = false;
char[] content = script.toCharArray();
for (int i = 0; i < script.length(); i++) {
if (content[i] == '\'') {
inLiteral = !inLiteral;
}
if (!inLiteral && script.startsWith(delim, i)) {
return true;
}
}
return false;
}
/**
* Execute the given SQL script using default settings for statement
* separators, comment delimiters, and exception handling flags.
*
* Statement separators and comments will be removed before executing
* individual statements within the supplied script.
*
* Warning: this method does not release the
* provided {@link Connection}.
*
* @param connection
* the JDBC connection to use to execute the script; already
* configured and ready to use
* @param resource
* the resource to load the SQL script from; encoded with the
* current platform's default encoding
* @throws ScriptException
* if an error occurred while executing the SQL script
* @see #executeSqlScript(Connection, EncodedResource, boolean, boolean,
* String, String, String, String)
* @see #DEFAULT_STATEMENT_SEPARATOR
* @see #DEFAULT_COMMENT_PREFIX
* @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
* @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
*/
public static void executeSqlScript(Connection connection, Resource resource) throws ScriptException {
executeSqlScript(connection, new EncodedResource(resource));
}
/**
* Execute the given SQL script using default settings for statement
* separators, comment delimiters, and exception handling flags.
*
* Statement separators and comments will be removed before executing
* individual statements within the supplied script.
*
* Warning: this method does not release the
* provided {@link Connection}.
*
* @param connection
* the JDBC connection to use to execute the script; already
* configured and ready to use
* @param resource
* the resource (potentially associated with a specific encoding)
* to load the SQL script from
* @throws ScriptException
* if an error occurred while executing the SQL script
* @see #executeSqlScript(Connection, EncodedResource, boolean, boolean,
* String, String, String, String)
* @see #DEFAULT_STATEMENT_SEPARATOR
* @see #DEFAULT_COMMENT_PREFIX
* @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
* @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
*/
public static void executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
executeSqlScript(connection, resource, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR,
DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
}
/**
* Execute the given SQL script.
*
* Statement separators and comments will be removed before executing
* individual statements within the supplied script.
*
* Warning: this method does not release the
* provided {@link Connection}.
*
* @param connection
* the JDBC connection to use to execute the script; already
* configured and ready to use
* @param resource
* the resource (potentially associated with a specific encoding)
* to load the SQL script from
* @param continueOnError
* whether or not to continue without throwing an exception in
* the event of an error
* @param ignoreFailedDrops
* whether or not to continue in the event of specifically an
* error on a {@code DROP} statement
* @param commentPrefix
* the prefix that identifies single-line comments in the SQL
* script — typically "--"
* @param separator
* the script statement separator; defaults to
* {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and
* falls back to {@value #FALLBACK_STATEMENT_SEPARATOR} as a last
* resort; may be set to {@value #EOF_STATEMENT_SEPARATOR} to
* signal that the script contains a single statement without a
* separator
* @param blockCommentStartDelimiter
* the start block comment delimiter; never {@code null}
* or empty
* @param blockCommentEndDelimiter
* the end block comment delimiter; never {@code null}
* or empty
* @throws ScriptException
* if an error occurred while executing the SQL script
* @see #DEFAULT_STATEMENT_SEPARATOR
* @see #FALLBACK_STATEMENT_SEPARATOR
* @see #EOF_STATEMENT_SEPARATOR
*/
public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
boolean ignoreFailedDrops, String commentPrefix, String separator, String blockCommentStartDelimiter,
String blockCommentEndDelimiter) throws ScriptException {
try {
if (logger.isInfoEnabled()) {
logger.info("Executing SQL script from " + resource);
}
long startTime = System.currentTimeMillis();
String script;
try {
script = readScript(resource, commentPrefix, separator);
} catch (IOException ex) {
throw new CannotReadScriptException(resource, ex);
}
if (separator == null) {
separator = DEFAULT_STATEMENT_SEPARATOR;
}
if (!EOF_STATEMENT_SEPARATOR.equals(separator) && !containsSqlScriptDelimiters(script, separator)) {
separator = FALLBACK_STATEMENT_SEPARATOR;
}
List statements = new LinkedList();
splitSqlScript(resource, script, separator, commentPrefix, blockCommentStartDelimiter,
blockCommentEndDelimiter, statements);
int stmtNumber = 0;
Statement stmt = connection.createStatement();
try {
for (String statement : statements) {
stmtNumber++;
try {
stmt.execute(statement);
int rowsAffected = stmt.getUpdateCount();
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState()
+ "', error code '" + warningToLog.getErrorCode() + "', message ["
+ warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
}
} catch (SQLException ex) {
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
if (continueOnError || (dropStatement && ignoreFailedDrops)) {
if (logger.isDebugEnabled()) {
logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber,
resource), ex);
}
} else {
throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex);
}
}
}
} finally {
try {
stmt.close();
} catch (Throwable ex) {
logger.debug("Could not close JDBC Statement", ex);
}
}
long elapsedTime = System.currentTimeMillis() - startTime;
if (logger.isInfoEnabled()) {
logger.info("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
}
} catch (Exception ex) {
if (ex instanceof ScriptException) {
throw (ScriptException) ex;
}
throw new UncategorizedScriptException("Failed to execute database script from resource [" + resource + "]",
ex);
}
}
}