From 17914fc44b1388ab755db28d7c6e47eb2f047d3c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 14 Jul 2019 10:33:42 +0100 Subject: [PATCH] Add multi-prefix comment support for SQL scripts Update `ResourceDatabasePopulator` and `ScriptUtils` so that more than one comment prefix can be used when processing SQL scripts. This feature is particularly useful when dealing with scripts provided by Quartz since they often use a mix `--` and `#`. Closes gh-23289 --- .../init/ResourceDatabasePopulator.java | 19 ++- .../jdbc/datasource/init/ScriptUtils.java | 134 ++++++++++++++++-- .../datasource/init/ScriptUtilsUnitTests.java | 18 ++- .../test-data-with-multi-prefix-comments.sql | 18 +++ 4 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-prefix-comments.sql 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 0216e39dad..50f19a1c41 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 class ResourceDatabasePopulator implements DatabasePopulator { * 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 class ResourceDatabasePopulator implements DatabasePopulator { 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 6f2d2128b0..478ee0a3e0 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 abstract class ScriptUtils { 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 @@ public abstract class ScriptUtils { 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 @@ public abstract class ScriptUtils { * @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 @@ public abstract class ScriptUtils { * 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 @@ public abstract class ScriptUtils { 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 @@ public abstract class ScriptUtils { } } + 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 abstract class ScriptUtils { 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 abstract class ScriptUtils { 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 abstract class ScriptUtils { } 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 ba15d9303b..ae60f813c6 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.ClassPathResource; 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; @@ -117,18 +120,25 @@ public class ScriptUtilsUnitTests { @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 { + @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(script, ';', statements); + 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)"; 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 0000000000..b7d34212ae --- /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 +);--