diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java index 0216e39dad28..50f19a1c411c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java @@ -60,7 +60,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator { private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR; - private String commentPrefix = ScriptUtils.DEFAULT_COMMENT_PREFIX; + private String[] commentPrefixes = ScriptUtils.DEFAULT_COMMENT_PREFIXES; private String blockCommentStartDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER; @@ -171,9 +171,22 @@ public void setSeparator(String separator) { * Set the prefix that identifies single-line comments within the SQL scripts. *

Defaults to {@code "--"}. * @param commentPrefix the prefix for single-line comments + * @see #setCommentPrefixes(String...) */ public void setCommentPrefix(String commentPrefix) { - this.commentPrefix = commentPrefix; + Assert.hasText(commentPrefix, "CommentPrefix must not be null or empty"); + this.commentPrefixes = new String[] { commentPrefix }; + } + + /** + * Set the prefixes that identify single-line comments within the SQL scripts. + *

Defaults to {@code "--"}. + * @param commentPrefixes the prefixes for single-line comments + * @since 5.2 + */ + public void setCommentPrefixes(String... commentPrefixes) { + Assert.notNull(commentPrefixes, "CommentPrefixes must not be null"); + this.commentPrefixes = commentPrefixes; } /** @@ -236,7 +249,7 @@ public void populate(Connection connection) throws ScriptException { for (Resource script : this.scripts) { EncodedResource encodedScript = new EncodedResource(script, this.sqlScriptEncoding); ScriptUtils.executeSqlScript(connection, encodedScript, this.continueOnError, this.ignoreFailedDrops, - this.commentPrefix, this.separator, this.blockCommentStartDelimiter, this.blockCommentEndDelimiter); + this.commentPrefixes, this.separator, this.blockCommentStartDelimiter, this.blockCommentEndDelimiter); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java index 6f2d2128b0f2..478ee0a3e0ed 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java @@ -80,6 +80,12 @@ public abstract class ScriptUtils { */ public static final String DEFAULT_COMMENT_PREFIX = "--"; + /** + * Default prefixes for single-line comments within SQL scripts: {@code ["--"]}. + * @since 5.2 + */ + public static final String[] DEFAULT_COMMENT_PREFIXES = { DEFAULT_COMMENT_PREFIX }; + /** * Default start delimiter for block comments within SQL scripts: {@code "/*"}. */ @@ -170,9 +176,46 @@ public static void splitSqlScript(@Nullable EncodedResource resource, String scr String separator, String commentPrefix, String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements) throws ScriptException { + Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty"); + splitSqlScript(resource, script, separator, new String[] { commentPrefix }, + blockCommentStartDelimiter, blockCommentEndDelimiter, 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 + * @param separator text separating each statement + * (typically a ';' or newline character) + * @param commentPrefixes the prefixes that identify SQL line comments + * (typically "--") + * @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 + * @since 5.2 + */ + public static void splitSqlScript(@Nullable EncodedResource resource, String script, + String separator, String[] commentPrefixes, 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.notNull(commentPrefixes, "'commentPrefixes' must not be null"); + for (int i = 0; i < commentPrefixes.length; i++) { + Assert.hasText(commentPrefixes[i], "'commentPrefixes' must not contain null or empty elements"); + } Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty"); Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty"); @@ -210,7 +253,7 @@ else if (!inSingleQuote && (c == '"')) { i += separator.length() - 1; continue; } - else if (script.startsWith(commentPrefix, i)) { + else if (startsWithAny(script, commentPrefixes, i)) { // Skip over any content from the start of the comment to the EOL int indexOfNextNewline = script.indexOf('\n', i); if (indexOfNextNewline > i) { @@ -260,7 +303,7 @@ else if (c == ' ' || c == '\r' || c == '\n' || c == '\t') { * @throws IOException in case of I/O errors */ static String readScript(EncodedResource resource) throws IOException { - return readScript(resource, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + return readScript(resource, DEFAULT_COMMENT_PREFIXES, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_END_DELIMITER); } /** @@ -271,19 +314,19 @@ static String readScript(EncodedResource resource) throws IOException { * 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 + * @param commentPrefixes the prefix that identifies comments in the SQL script * (typically "--") * @param separator the statement separator in the SQL script (typically ";") * @param blockCommentEndDelimiter the end block comment delimiter * @return a {@code String} containing the script lines * @throws IOException in case of I/O errors */ - private static String readScript(EncodedResource resource, @Nullable String commentPrefix, + private static String readScript(EncodedResource resource, @Nullable String[] commentPrefixes, @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException { LineNumberReader lnr = new LineNumberReader(resource.getReader()); try { - return readScript(lnr, commentPrefix, separator, blockCommentEndDelimiter); + return readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter); } finally { lnr.close(); @@ -309,11 +352,35 @@ private static String readScript(EncodedResource resource, @Nullable String comm public static String readScript(LineNumberReader lineNumberReader, @Nullable String lineCommentPrefix, @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException { + String[] lineCommentPrefixes = (lineCommentPrefix != null) ? new String[] { lineCommentPrefix } : null; + return readScript(lineNumberReader, lineCommentPrefixes, separator, blockCommentEndDelimiter); + } + + /** + * 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 lineCommentPrefixes the prefixes that identify comments in the SQL script + * (typically "--") + * @param separator the statement separator in the SQL script (typically ";") + * @param blockCommentEndDelimiter the end block comment delimiter + * @return a {@code String} containing the script lines + * @throws IOException in case of I/O errors + * @since 5.2 + */ + public static String readScript(LineNumberReader lineNumberReader, @Nullable String[] lineCommentPrefixes, + @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException { + String currentStatement = lineNumberReader.readLine(); StringBuilder scriptBuilder = new StringBuilder(); while (currentStatement != null) { if ((blockCommentEndDelimiter != null && currentStatement.contains(blockCommentEndDelimiter)) || - (lineCommentPrefix != null && !currentStatement.startsWith(lineCommentPrefix))) { + (lineCommentPrefixes != null && !startsWithAny(currentStatement, lineCommentPrefixes, 0))) { if (scriptBuilder.length() > 0) { scriptBuilder.append('\n'); } @@ -340,6 +407,15 @@ private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuild } } + private static boolean startsWithAny(String script, String[] prefixes, int toffset) { + for (String prefix : prefixes) { + if (script.startsWith(prefix, toffset)) { + return true; + } + } + return false; + } + /** * Does the provided SQL script contain the specified delimiter? * @param script the SQL script @@ -454,6 +530,46 @@ public static void executeSqlScript(Connection connection, EncodedResource resou boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator, String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException { + executeSqlScript(connection, resource, continueOnError, ignoreFailedDrops, + new String[] { commentPrefix }, separator, blockCommentStartDelimiter, + blockCommentEndDelimiter); + } + + /** + * 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 commentPrefixes the prefixes that identify 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 + * @param blockCommentEndDelimiter the end block comment delimiter + * @throws ScriptException if an error occurred while executing the SQL script + * @since 5.2 + * @see #DEFAULT_STATEMENT_SEPARATOR + * @see #FALLBACK_STATEMENT_SEPARATOR + * @see #EOF_STATEMENT_SEPARATOR + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError, + boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator, + String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException { + try { if (logger.isDebugEnabled()) { logger.debug("Executing SQL script from " + resource); @@ -462,7 +578,7 @@ public static void executeSqlScript(Connection connection, EncodedResource resou String script; try { - script = readScript(resource, commentPrefix, separator, blockCommentEndDelimiter); + script = readScript(resource, commentPrefixes, separator, blockCommentEndDelimiter); } catch (IOException ex) { throw new CannotReadScriptException(resource, ex); @@ -476,7 +592,7 @@ public static void executeSqlScript(Connection connection, EncodedResource resou } List statements = new ArrayList<>(); - splitSqlScript(resource, script, separator, commentPrefix, blockCommentStartDelimiter, + splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter, blockCommentEndDelimiter, statements); int stmtNumber = 0; diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java index ba15d9303b0b..2922bbae4d18 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java @@ -25,6 +25,9 @@ import org.springframework.core.io.support.EncodedResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER; +import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER; +import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_COMMENT_PREFIXES; import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_STATEMENT_SEPARATOR; import static org.springframework.jdbc.datasource.init.ScriptUtils.containsSqlScriptDelimiters; import static org.springframework.jdbc.datasource.init.ScriptUtils.splitSqlScript; @@ -54,10 +57,7 @@ public void splitSqlScriptDelimitedWithSemicolon() { String script = rawStatement1 + delim + rawStatement2 + delim + rawStatement3 + delim; List statements = new ArrayList<>(); splitSqlScript(script, delim, statements); - assertThat(statements.size()).as("wrong number of statements").isEqualTo(3); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(cleanedStatement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(cleanedStatement2); - assertThat(statements.get(2)).as("statement 3 not split correctly").isEqualTo(cleanedStatement3); + assertThat(statements).containsExactly(cleanedStatement1, cleanedStatement2, cleanedStatement3); } @Test @@ -69,10 +69,7 @@ public void splitSqlScriptDelimitedWithNewLine() { String script = statement1 + delim + statement2 + delim + statement3 + delim; List statements = new ArrayList<>(); splitSqlScript(script, delim, statements); - assertThat(statements.size()).as("wrong number of statements").isEqualTo(3); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); - assertThat(statements.get(2)).as("statement 3 not split correctly").isEqualTo(statement3); + assertThat(statements).containsExactly(statement1, statement2, statement3); } @Test @@ -83,8 +80,7 @@ public void splitSqlScriptDelimitedWithNewLineButDefaultDelimiterSpecified() { String script = statement1 + delim + statement2 + delim; List statements = new ArrayList<>(); splitSqlScript(script, DEFAULT_STATEMENT_SEPARATOR, statements); - assertThat(statements.size()).as("wrong number of statements").isEqualTo(1); - assertThat(statements.get(0)).as("script should have been 'stripped' but not actually 'split'").isEqualTo(script.replace('\n', ' ')); + assertThat(statements).as("stripped but not split statements").containsExactly(script.replace('\n', ' ')); } @Test // SPR-13218 @@ -95,9 +91,7 @@ public void splitScriptWithSingleQuotesNestedInsideDoubleQuotes() throws Excepti String script = statement1 + delim + statement2 + delim; List statements = new ArrayList<>(); splitSqlScript(script, ';', statements); - assertThat(statements.size()).as("wrong number of statements").isEqualTo(2); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); + assertThat(statements).containsExactly(statement1, statement2); } @Test // SPR-11560 @@ -105,42 +99,39 @@ public void readAndSplitScriptWithMultipleNewlinesAsSeparator() throws Exception String script = readScript("db-test-data-multi-newline.sql"); List statements = new ArrayList<>(); splitSqlScript(script, "\n\n", statements); - String statement1 = "insert into T_TEST (NAME) values ('Keith')"; String statement2 = "insert into T_TEST (NAME) values ('Dave')"; - - assertThat(statements.size()).as("wrong number of statements").isEqualTo(2); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); + assertThat(statements).containsExactly(statement1, statement2); } @Test public void readAndSplitScriptContainingComments() throws Exception { String script = readScript("test-data-with-comments.sql"); - splitScriptContainingComments(script); + splitScriptContainingComments(script, DEFAULT_COMMENT_PREFIXES); } @Test public void readAndSplitScriptContainingCommentsWithWindowsLineEnding() throws Exception { String script = readScript("test-data-with-comments.sql").replaceAll("\n", "\r\n"); - splitScriptContainingComments(script); + splitScriptContainingComments(script, DEFAULT_COMMENT_PREFIXES); } - private void splitScriptContainingComments(String script) throws Exception { - List statements = new ArrayList<>(); - splitSqlScript(script, ';', statements); + @Test + public void readAndSplitScriptContainingCommentsWithMultiplePrefixes() throws Exception { + String script = readScript("test-data-with-multi-prefix-comments.sql"); + splitScriptContainingComments(script, "--", "#", "^"); + } + private void splitScriptContainingComments(String script, String... commentPrefixes) throws Exception { + List statements = new ArrayList<>(); + splitSqlScript(null, script, ";", commentPrefixes, DEFAULT_BLOCK_COMMENT_START_DELIMITER, + DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements); String statement1 = "insert into customer (id, name) values (1, 'Rod; Johnson'), (2, 'Adrian Collier')"; String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; // Statement 4 addresses the error described in SPR-9982. String statement4 = "INSERT INTO persons( person_id , name) VALUES( 1 , 'Name' )"; - - assertThat(statements.size()).as("wrong number of statements").isEqualTo(4); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); - assertThat(statements.get(2)).as("statement 3 not split correctly").isEqualTo(statement3); - assertThat(statements.get(3)).as("statement 4 not split correctly").isEqualTo(statement4); + assertThat(statements).containsExactly(statement1, statement2, statement3, statement4); } @Test // SPR-10330 @@ -148,15 +139,10 @@ public void readAndSplitScriptContainingCommentsWithLeadingTabs() throws Excepti String script = readScript("test-data-with-comments-and-leading-tabs.sql"); List statements = new ArrayList<>(); splitSqlScript(script, ';', statements); - String statement1 = "insert into customer (id, name) values (1, 'Sam Brannen')"; String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2013-06-08', 1)"; String statement3 = "insert into orders(id, order_date, customer_id) values (2, '2013-06-08', 1)"; - - assertThat(statements.size()).as("wrong number of statements").isEqualTo(3); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); - assertThat(statements.get(2)).as("statement 3 not split correctly").isEqualTo(statement3); + assertThat(statements).containsExactly(statement1, statement2, statement3); } @Test // SPR-9531 @@ -164,13 +150,9 @@ public void readAndSplitScriptContainingMultiLineComments() throws Exception { String script = readScript("test-data-with-multi-line-comments.sql"); List statements = new ArrayList<>(); splitSqlScript(script, ';', statements); - String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller')"; String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Sam' , 'Brannen' )"; - - assertThat(statements.size()).as("wrong number of statements").isEqualTo(2); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); + assertThat(statements).containsExactly(statement1, statement2); } @Test @@ -178,26 +160,19 @@ public void readAndSplitScriptContainingMultiLineNestedComments() throws Excepti String script = readScript("test-data-with-multi-line-nested-comments.sql"); List statements = new ArrayList<>(); splitSqlScript(script, ';', statements); - String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller')"; String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Sam' , 'Brannen' )"; - - assertThat(statements.size()).as("wrong number of statements").isEqualTo(2); - assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); - assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); + assertThat(statements).containsExactly(statement1, statement2); } @Test public void containsDelimiters() { assertThat(containsSqlScriptDelimiters("select 1\n select ';'", ";")).isFalse(); assertThat(containsSqlScriptDelimiters("select 1; select 2", ";")).isTrue(); - assertThat(containsSqlScriptDelimiters("select 1; select '\\n\n';", "\n")).isFalse(); assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n")).isTrue(); - assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n\n")).isFalse(); assertThat(containsSqlScriptDelimiters("select 1\n\n select 2", "\n\n")).isTrue(); - // MySQL style escapes '\\' assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('a\\\\', 'b;')", ";")).isFalse(); assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('Charles', 'd\\'Artagnan'); select 1;", ";")).isTrue(); diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-prefix-comments.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-prefix-comments.sql new file mode 100644 index 000000000000..b7d34212ae3b --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-prefix-comments.sql @@ -0,0 +1,18 @@ +-- The next comment line has no text after the '--' prefix. +-- +-- The next comment line starts with a space. + -- x, y, z... + +insert into customer (id, name) +values (1, 'Rod; Johnson'), (2, 'Adrian Collier'); +-- This is also a comment. +insert into orders(id, order_date, customer_id) +values (1, '2008-01-02', 2); +# A comment with a different prefix +insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2); +INSERT INTO persons( person_id-- + , name) +^ A comment with yet another different prefix +VALUES( 1 -- person_id + , 'Name' --name +);--