From 198fa8ce3fe8a3ec76a41e9d79cf5dc1f692e4be Mon Sep 17 00:00:00 2001 From: Eugeny Karpov Date: Sat, 3 Feb 2018 20:36:15 +0000 Subject: [PATCH] #551: Abstracted and changed database init script functionality to support use of SQL-like scripts with non-JDBC connections. Supports #525 --- CHANGELOG.md | 6 +- modules/database-commons/pom.xml | 23 ++ .../delegate/AbstractDatabaseDelegate.java | 55 +++ .../delegate/DatabaseDelegate.java | 31 ++ .../ConnectionCreationException.java | 13 + .../org/testcontainers/ext/ScriptUtils.java | 298 +++++++++++++++ modules/jdbc/pom.xml | 6 + .../jdbc/ContainerDatabaseDriver.java | 12 +- .../jdbc/ContainerLessJdbcDelegate.java | 36 ++ .../jdbc/JdbcDatabaseDelegate.java | 60 +++ .../testcontainers/jdbc/ext/ScriptUtils.java | 345 +++--------------- pom.xml | 1 + 12 files changed, 592 insertions(+), 294 deletions(-) create mode 100644 modules/database-commons/pom.xml create mode 100644 modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java create mode 100644 modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java create mode 100644 modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java create mode 100644 modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java create mode 100644 modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerLessJdbcDelegate.java create mode 100644 modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 97bfdc666cb..aeb90f7cbe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,14 @@ # Change Log All notable changes to this project will be documented in this file. -## [1.6.1] - 2018-01-31 +## UNRELEASED ### Fixed - - Fixed extraneous insertion of `useSSL=false` in all JDBC URL strings, even for DBs that do not understand it. Usage is now restricted to MySQL by default and can be overridden by authors of `JdbcDatabaseContainer` subclasses ([\#568](https://github.com/testcontainers/testcontainers-java/issues/568)) +### Changed +- Abstracted and changed database init script functionality to support use of SQL-like scripts with non-JDBC connections. ([\#551](https://github.com/testcontainers/testcontainers-java/pull/551)) + ## [1.6.0] - 2018-01-28 ### Fixed diff --git a/modules/database-commons/pom.xml b/modules/database-commons/pom.xml new file mode 100644 index 00000000000..965c0c0070c --- /dev/null +++ b/modules/database-commons/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + org.testcontainers + testcontainers-parent + 0-SNAPSHOT + ../../pom.xml + + + database-commons + TestContainers :: Database-Commons + + + + ${project.groupId} + testcontainers + ${project.version} + + + + diff --git a/modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java b/modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java new file mode 100644 index 00000000000..14572dec421 --- /dev/null +++ b/modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java @@ -0,0 +1,55 @@ +package org.testcontainers.delegate; + +import java.util.Collection; + +/** + * @param connection to the database + * @author Eugeny Karpov + */ +public abstract class AbstractDatabaseDelegate implements DatabaseDelegate { + + /** + * Database connection + */ + private CONNECTION connection; + + private boolean isConnectionStarted = false; + + /** + * Get or create new connection to the database + */ + protected CONNECTION getConnection() { + if (!isConnectionStarted) { + connection = createNewConnection(); + isConnectionStarted = true; + } + return connection; + } + + @Override + public void execute(Collection statements, String scriptPath, boolean continueOnError, boolean ignoreFailedDrops) { + int lineNumber = 0; + for (String statement : statements) { + lineNumber++; + execute(statement, scriptPath, lineNumber, continueOnError, ignoreFailedDrops); + } + } + + @Override + public void close() { + if (isConnectionStarted) { + closeConnectionQuietly(connection); + isConnectionStarted = false; + } + } + + /** + * Quietly close the connection + */ + protected abstract void closeConnectionQuietly(CONNECTION connection); + + /** + * Template method for creating new connections to the database + */ + protected abstract CONNECTION createNewConnection(); +} diff --git a/modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java b/modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java new file mode 100644 index 00000000000..55886d88143 --- /dev/null +++ b/modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java @@ -0,0 +1,31 @@ +package org.testcontainers.delegate; + +import java.util.Collection; + +/** + * Database delegate + * + * Gives an abstraction from concrete database + * + * @author Eugeny Karpov + */ +public interface DatabaseDelegate extends AutoCloseable { + + /** + * Execute statement by the implementation of the delegate + */ + void execute(String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops); + + /** + * Execute collection of statements + */ + void execute(Collection statements, String scriptPath, boolean continueOnError, boolean ignoreFailedDrops); + + /** + * Close connection to the database + * + * Overridden to suppress throwing Exception + */ + @Override + void close(); +} diff --git a/modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java b/modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java new file mode 100644 index 00000000000..3b8852dbddc --- /dev/null +++ b/modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java @@ -0,0 +1,13 @@ +package org.testcontainers.exception; + +/** + * Inability to create connection to the database + * + * @author Eugeny Karpov + */ +public class ConnectionCreationException extends RuntimeException { + + public ConnectionCreationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java new file mode 100644 index 00000000000..e9fa2d18a80 --- /dev/null +++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java @@ -0,0 +1,298 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * 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. + */ + +package org.testcontainers.ext; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.delegate.DatabaseDelegate; + +import javax.script.ScriptException; +import java.util.LinkedList; +import java.util.List; + +/** + * This is a modified version of the Spring-JDBC ScriptUtils class, adapted to reduce + * dependencies and slightly alter the API. + * + * Generic utility methods for working with SQL scripts. Mainly for internal use + * 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 + * @since 4.0.3 + */ +public abstract class ScriptUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(ScriptUtils.class); + + /** + * Default statement separator within SQL scripts. + */ + public static final String DEFAULT_STATEMENT_SEPARATOR = ";"; + + /** + * Fallback statement separator within SQL scripts. + *

Used if neither a custom defined separator nor the + * {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script. + */ + public static final String FALLBACK_STATEMENT_SEPARATOR = "\n"; + + /** + * Default prefix for line comments within SQL scripts. + */ + public static final String DEFAULT_COMMENT_PREFIX = "--"; + + /** + * Default start delimiter for block comments within SQL scripts. + */ + 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 = "*/"; + + + /** + * Prevent instantiation of this utility class. + */ + private ScriptUtils() { + /* no-op */ + } + + /** + * 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 + */ + public static void splitSqlScript(String resource, String script, String separator, String commentPrefix, + String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements) { + + checkArgument(StringUtils.isNotEmpty(script), "script must not be null or empty"); + checkArgument(separator != null, "separator must not be null"); + checkArgument(StringUtils.isNotEmpty(commentPrefix), "commentPrefix must not be null or empty"); + checkArgument(StringUtils.isNotEmpty(blockCommentStartDelimiter), "blockCommentStartDelimiter must not be null or empty"); + checkArgument(StringUtils.isNotEmpty(blockCommentEndDelimiter), "blockCommentEndDelimiter must not be null or empty"); + + StringBuilder sb = new StringBuilder(); + boolean inLiteral = 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 (c == '\'') { + inLiteral = !inLiteral; + } + if (!inLiteral) { + 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.isNotEmpty(sb.toString())) { + statements.add(sb.toString()); + } + } + + private static void checkArgument(boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalArgumentException(errorMessage); + } + } + + /** + * 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; + } + + public static void executeDatabaseScript(DatabaseDelegate databaseDelegate, String scriptPath, String script) throws ScriptException { + executeDatabaseScript(databaseDelegate, scriptPath, script, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + } + + /** + * Execute the given database script. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Do not use this method to execute DDL if you expect rollback. + * @param databaseDelegate database delegate for script execution + * @param scriptPath the resource (potentially associated with a specific encoding) + * to load the SQL script from + * @param script the raw script content + *@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 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 + * @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 + */ + public static void executeDatabaseScript(DatabaseDelegate databaseDelegate, String scriptPath, String script, boolean continueOnError, + boolean ignoreFailedDrops, String commentPrefix, String separator, String blockCommentStartDelimiter, + String blockCommentEndDelimiter) throws ScriptException { + + try { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Executing database script from " + scriptPath); + } + + long startTime = System.currentTimeMillis(); + List statements = new LinkedList<>(); + + if (separator == null) { + separator = DEFAULT_STATEMENT_SEPARATOR; + } + if (!containsSqlScriptDelimiters(script, separator)) { + separator = FALLBACK_STATEMENT_SEPARATOR; + } + + splitSqlScript(scriptPath, script, separator, commentPrefix, blockCommentStartDelimiter, + blockCommentEndDelimiter, statements); + + try (DatabaseDelegate closeableDelegate = databaseDelegate) { + closeableDelegate.execute(statements, scriptPath, continueOnError, ignoreFailedDrops); + } + + long elapsedTime = System.currentTimeMillis() - startTime; + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Executed database script from " + scriptPath + " in " + elapsedTime + " ms."); + } + } + catch (Exception ex) { + if (ex instanceof ScriptException) { + throw (ScriptException) ex; + } + + throw new UncategorizedScriptException( + "Failed to execute database script from resource [" + script + "]", ex); + } + } + + private static class ScriptParseException extends RuntimeException { + public ScriptParseException(String format, String scriptPath) { + super(String.format(format, scriptPath)); + } + } + + public static class ScriptStatementFailedException extends RuntimeException { + public ScriptStatementFailedException(String statement, int lineNumber, String scriptPath, Exception ex) { + super(String.format("Script execution failed (%s:%d): %s", scriptPath, lineNumber, statement), ex); + } + } + + private static class UncategorizedScriptException extends RuntimeException { + public UncategorizedScriptException(String s, Exception ex) { + super(s, ex); + } + } +} diff --git a/modules/jdbc/pom.xml b/modules/jdbc/pom.xml index 0fe42c3f4f0..72614a410e6 100644 --- a/modules/jdbc/pom.xml +++ b/modules/jdbc/pom.xml @@ -18,6 +18,12 @@ testcontainers ${project.version} + + + ${project.groupId} + database-commons + ${project.version} + diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java index d965fb83d03..b7bd9fb4925 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java @@ -4,7 +4,8 @@ import org.slf4j.LoggerFactory; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; -import org.testcontainers.jdbc.ext.ScriptUtils; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.ext.ScriptUtils; import javax.script.ScriptException; import java.io.IOException; @@ -153,7 +154,8 @@ public synchronized Connection connect(String url, final Properties info) throws an init script or function has been specified, use it */ if (!initializedContainers.contains(container.getContainerId())) { - runInitScriptIfRequired(url, connection); + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(container); + runInitScriptIfRequired(url, databaseDelegate); runInitFunctionIfRequired(url, connection); initializedContainers.add(container.getContainerId()); } @@ -214,10 +216,10 @@ private Connection wrapConnection(final Connection connection, final JdbcDatabas * Run an init script from the classpath. * * @param url the JDBC URL to check for init script declarations. - * @param connection JDBC connection to apply init scripts to. + * @param databaseDelegate database delegate to apply init scripts to the database * @throws SQLException on script or DB error */ - private void runInitScriptIfRequired(String url, Connection connection) throws SQLException { + private void runInitScriptIfRequired(String url, DatabaseDelegate databaseDelegate) throws SQLException { Matcher matcher = INITSCRIPT_MATCHING_PATTERN.matcher(url); if (matcher.matches()) { String initScriptPath = matcher.group(2); @@ -230,7 +232,7 @@ private void runInitScriptIfRequired(String url, Connection connection) throws S } String sql = IOUtils.toString(resource, StandardCharsets.UTF_8); - ScriptUtils.executeSqlScript(connection, initScriptPath, sql); + ScriptUtils.executeDatabaseScript(databaseDelegate, initScriptPath, sql); } catch (IOException e) { LOGGER.warn("Could not load classpath init script: {}", initScriptPath); throw new SQLException("Could not load classpath init script: " + initScriptPath, e); diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerLessJdbcDelegate.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerLessJdbcDelegate.java new file mode 100644 index 00000000000..7a22d139bea --- /dev/null +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerLessJdbcDelegate.java @@ -0,0 +1,36 @@ +package org.testcontainers.jdbc; + +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.exception.ConnectionCreationException; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Containerless jdbc database delegate + * + * Is used only with deprecated ScriptUtils + * + * @see org.testcontainers.ext.ScriptUtils + */ +@Slf4j +public class ContainerLessJdbcDelegate extends JdbcDatabaseDelegate { + + private Connection connection; + + public ContainerLessJdbcDelegate(Connection connection) { + super(null); + this.connection = connection; + } + + @Override + protected Statement createNewConnection() { + try { + return connection.createStatement(); + } catch (SQLException e) { + log.error("Could create JDBC statement"); + throw new ConnectionCreationException("Could create JDBC statement", e); + } + } +} diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java new file mode 100644 index 00000000000..e490da87c4a --- /dev/null +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java @@ -0,0 +1,60 @@ +package org.testcontainers.jdbc; + +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.delegate.AbstractDatabaseDelegate; +import org.testcontainers.exception.ConnectionCreationException; +import org.testcontainers.ext.ScriptUtils; + +import java.sql.SQLException; +import java.sql.Statement; + +/** + * JDBC database delegate + * + * @author Eugeny Karpov + */ +@Slf4j +public class JdbcDatabaseDelegate extends AbstractDatabaseDelegate { + + private JdbcDatabaseContainer container; + + public JdbcDatabaseDelegate(JdbcDatabaseContainer container) { + this.container = container; + } + + @Override + protected Statement createNewConnection() { + try { + return container.createConnection("").createStatement(); + } catch (SQLException e) { + log.error("Could not obtain JDBC connection"); + throw new ConnectionCreationException("Could not obtain JDBC connection", e); + } + } + + + @Override + public void execute(String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops) { + try { + boolean rowsAffected = getConnection().execute(statement); + log.debug("{} returned as updateCount for SQL: {}", rowsAffected, statement); + } catch (SQLException ex) { + boolean dropStatement = statement.trim().toLowerCase().startsWith("drop"); + if (continueOnError || (dropStatement && ignoreFailedDrops)) { + log.debug("Failed to execute SQL script statement at line {} of resource {}: {}", lineNumber, scriptPath, statement, ex); + } else { + throw new ScriptUtils.ScriptStatementFailedException(statement, lineNumber, scriptPath, ex); + } + } + } + + @Override + protected void closeConnectionQuietly(Statement statement) { + try { + statement.close(); + } catch (Exception e) { + log.error("Could not close JDBC connection", e); + } + } +} diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ext/ScriptUtils.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ext/ScriptUtils.java index 286ea324f42..76b7a044c9d 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ext/ScriptUtils.java +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ext/ScriptUtils.java @@ -16,316 +16,87 @@ package org.testcontainers.jdbc.ext; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.testcontainers.jdbc.ContainerLessJdbcDelegate; import javax.script.ScriptException; import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.LinkedList; import java.util.List; /** - * This is a modified version of the Spring-JDBC ScriptUtils class, adapted to reduce - * dependencies and slightly alter the API. + * Wrapper for database-agnostic ScriptUtils * - * Generic utility methods for working with SQL scripts. Mainly for internal use - * 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 - * @since 4.0.3 + * @see org.testcontainers.ext.ScriptUtils + * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils */ public abstract class ScriptUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(ScriptUtils.class); - - /** - * Default statement separator within SQL scripts. - */ - public static final String DEFAULT_STATEMENT_SEPARATOR = ";"; - - /** - * Fallback statement separator within SQL scripts. - *

Used if neither a custom defined separator nor the - * {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script. - */ - public static final String FALLBACK_STATEMENT_SEPARATOR = "\n"; - - /** - * Default prefix for line comments within SQL scripts. - */ - public static final String DEFAULT_COMMENT_PREFIX = "--"; - - /** - * Default start delimiter for block comments within SQL scripts. - */ - 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 = "*/"; - - - /** - * Prevent instantiation of this utility class. - */ - private ScriptUtils() { - /* no-op */ - } - /** - * 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 - */ - public static void splitSqlScript(String resource, String script, String separator, String commentPrefix, - String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements) { + * Default statement separator within SQL scripts. + */ + public static final String DEFAULT_STATEMENT_SEPARATOR = ";"; - checkArgument(StringUtils.isNotEmpty(script), "script must not be null or empty"); - checkArgument(separator != null, "separator must not be null"); - checkArgument(StringUtils.isNotEmpty(commentPrefix), "commentPrefix must not be null or empty"); - checkArgument(StringUtils.isNotEmpty(blockCommentStartDelimiter), "blockCommentStartDelimiter must not be null or empty"); - checkArgument(StringUtils.isNotEmpty(blockCommentEndDelimiter), "blockCommentEndDelimiter must not be null or empty"); + /** + * Fallback statement separator within SQL scripts. + *

Used if neither a custom defined separator nor the + * {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script. + */ + public static final String FALLBACK_STATEMENT_SEPARATOR = "\n"; - StringBuilder sb = new StringBuilder(); - boolean inLiteral = 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 (c == '\'') { - inLiteral = !inLiteral; - } - if (!inLiteral) { - 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.isNotEmpty(sb.toString())) { - statements.add(sb.toString()); - } - } + /** + * Default prefix for line comments within SQL scripts. + */ + public static final String DEFAULT_COMMENT_PREFIX = "--"; - private static void checkArgument(boolean expression, String errorMessage) { - if (!expression) { - throw new IllegalArgumentException(errorMessage); - } - } + /** + * Default start delimiter for block comments within SQL scripts. + */ + public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*"; /** - * 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; - } + * Default end delimiter for block comments within SQL scripts. + */ + public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/"; - public static void executeSqlScript(Connection connection, String scriptPath, String script) throws ScriptException { - executeSqlScript(connection, scriptPath, script, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + /** + * Prevent instantiation of this utility class. + */ + private ScriptUtils() { + /* no-op */ } /** - * Execute the given SQL script. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Do not use this method to execute DDL if you expect rollback. - * @param connection the JDBC connection to use to execute the script; already - * configured and ready to use - * @param scriptPath the resource (potentially associated with a specific encoding) - * to load the SQL script from - * @param script the raw script content - *@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 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 - * @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 - */ - public static void executeSqlScript(Connection connection, String scriptPath, String script, boolean continueOnError, - boolean ignoreFailedDrops, String commentPrefix, String separator, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { - - try { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Executing SQL script from " + scriptPath); - } - - long startTime = System.currentTimeMillis(); - List statements = new LinkedList<>(); - - if (separator == null) { - separator = DEFAULT_STATEMENT_SEPARATOR; - } - if (!containsSqlScriptDelimiters(script, separator)) { - separator = FALLBACK_STATEMENT_SEPARATOR; - } - - splitSqlScript(scriptPath, script, separator, commentPrefix, blockCommentStartDelimiter, - blockCommentEndDelimiter, statements); - int lineNumber = 0; - Statement stmt = connection.createStatement(); - try { - for (String statement : statements) { - lineNumber++; - try { - stmt.execute(statement); - int rowsAffected = stmt.getUpdateCount(); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(rowsAffected + " returned as updateCount for SQL: " + statement); - } - } - catch (SQLException ex) { - boolean dropStatement = statement.trim().toLowerCase().startsWith("drop"); - if (continueOnError || (dropStatement && ignoreFailedDrops)) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Failed to execute SQL script statement at line " + lineNumber - + " of resource " + scriptPath + ": " + statement, ex); - } - } - else { - throw new ScriptStatementFailedException(statement, lineNumber, scriptPath, 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 " + scriptPath + " in " + elapsedTime + " ms."); - } - } - catch (Exception ex) { - if (ex instanceof ScriptException) { - throw (ScriptException) ex; - } - - throw new UncategorizedScriptException( - "Failed to execute database script from resource [" + script + "]", ex); - } - } + * @see org.testcontainers.ext.ScriptUtils + * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils + */ + public static void splitSqlScript(String resource, String script, String separator, String commentPrefix, + String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements) { + org.testcontainers.ext.ScriptUtils.splitSqlScript(resource, script, separator, commentPrefix, blockCommentStartDelimiter, blockCommentEndDelimiter, statements); + } - private static class ScriptParseException extends RuntimeException { - public ScriptParseException(String format, String scriptPath) { - super(String.format(format, scriptPath)); - } + /** + * @see org.testcontainers.ext.ScriptUtils + * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils + */ + public static boolean containsSqlScriptDelimiters(String script, String delim) { + return org.testcontainers.ext.ScriptUtils.containsSqlScriptDelimiters(script, delim); } - private static class ScriptStatementFailedException extends RuntimeException { - public ScriptStatementFailedException(String statement, int lineNumber, String scriptPath, SQLException ex) { - super(String.format("Script execution failed (%s:%d): %s", scriptPath, lineNumber, statement), ex); - } + /** + * @see org.testcontainers.ext.ScriptUtils + * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils + */ + public static void executeSqlScript(Connection connection, String scriptPath, String script) throws ScriptException { + org.testcontainers.ext.ScriptUtils.executeDatabaseScript(new ContainerLessJdbcDelegate(connection), scriptPath, script); } - private static class UncategorizedScriptException extends RuntimeException { - public UncategorizedScriptException(String s, Exception ex) { - super(s, ex); - } + /** + * @see org.testcontainers.ext.ScriptUtils + * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils + */ + public static void executeSqlScript(Connection connection, String scriptPath, String script, boolean continueOnError, + boolean ignoreFailedDrops, String commentPrefix, String separator, String blockCommentStartDelimiter, + String blockCommentEndDelimiter) throws ScriptException { + org.testcontainers.ext.ScriptUtils.executeDatabaseScript(new ContainerLessJdbcDelegate(connection), scriptPath, + script, continueOnError, ignoreFailedDrops, commentPrefix, separator, blockCommentStartDelimiter, blockCommentEndDelimiter); } -} +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 90453b8839d..d291d174523 100644 --- a/pom.xml +++ b/pom.xml @@ -167,6 +167,7 @@ modules/nginx modules/kafka modules/jdbc-test + modules/database-commons