diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java index 24cfb59722..e4c3abd262 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2019 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. @@ -16,11 +16,15 @@ package org.springframework.test.context.jdbc; +import java.lang.reflect.Array; +import java.util.Arrays; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.style.ToStringCreator; import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.lang.Nullable; import org.springframework.test.context.jdbc.SqlConfig.ErrorMode; import org.springframework.test.context.jdbc.SqlConfig.TransactionMode; import org.springframework.util.Assert; @@ -37,6 +41,11 @@ import org.springframework.util.Assert; */ class MergedSqlConfig { + private static final String COMMENT_PREFIX = "commentPrefix"; + + private static final String COMMENT_PREFIXES = "commentPrefixes"; + + private final String dataSource; private final String transactionManager; @@ -47,7 +56,7 @@ class MergedSqlConfig { private final String separator; - private final String commentPrefix; + private final String[] commentPrefixes; private final String blockCommentStartDelimiter; @@ -68,38 +77,53 @@ class MergedSqlConfig { Assert.notNull(localSqlConfig, "Local @SqlConfig must not be null"); Assert.notNull(testClass, "testClass must not be null"); + AnnotationAttributes mergedAttributes; + AnnotationAttributes localAttributes = AnnotationUtils.getAnnotationAttributes(localSqlConfig, false, false); + // Enforce comment prefix aliases within the local @SqlConfig. + enforceCommentPrefixAliases(localAttributes); + // Get global attributes, if any. - AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes( + AnnotationAttributes globalAttributes = AnnotatedElementUtils.findMergedAnnotationAttributes( testClass, SqlConfig.class.getName(), false, false); - // Override global attributes with local attributes. - if (attributes != null) { - for (String key : attributes.keySet()) { - Object value = AnnotationUtils.getValue(localSqlConfig, key); - if (value != null) { - // Is the value explicit (i.e., not a 'default')? - if (!value.equals("") && value != TransactionMode.DEFAULT && value != ErrorMode.DEFAULT) { - attributes.put(key, value); + if (globalAttributes != null) { + // Enforce comment prefix aliases within the global @SqlConfig. + enforceCommentPrefixAliases(globalAttributes); + + for (String key : globalAttributes.keySet()) { + Object value = localAttributes.get(key); + if (isExplicitValue(value)) { + // Override global attribute with local attribute. + globalAttributes.put(key, value); + + // Ensure comment prefix aliases are honored during the merge. + if (key.equals(COMMENT_PREFIX) && isEmptyArray(localAttributes.get(COMMENT_PREFIXES))) { + globalAttributes.put(COMMENT_PREFIXES, value); + } + else if (key.equals(COMMENT_PREFIXES) && isEmptyString(localAttributes.get(COMMENT_PREFIX))) { + globalAttributes.put(COMMENT_PREFIX, value); } } } + mergedAttributes = globalAttributes; } else { // Otherwise, use local attributes only. - attributes = AnnotationUtils.getAnnotationAttributes(localSqlConfig, false, false); + mergedAttributes = localAttributes; } - this.dataSource = attributes.getString("dataSource"); - this.transactionManager = attributes.getString("transactionManager"); - this.transactionMode = getEnum(attributes, "transactionMode", TransactionMode.DEFAULT, TransactionMode.INFERRED); - this.encoding = attributes.getString("encoding"); - this.separator = getString(attributes, "separator", ScriptUtils.DEFAULT_STATEMENT_SEPARATOR); - this.commentPrefix = getString(attributes, "commentPrefix", ScriptUtils.DEFAULT_COMMENT_PREFIX); - this.blockCommentStartDelimiter = getString(attributes, "blockCommentStartDelimiter", + this.dataSource = mergedAttributes.getString("dataSource"); + this.transactionManager = mergedAttributes.getString("transactionManager"); + this.transactionMode = getEnum(mergedAttributes, "transactionMode", TransactionMode.DEFAULT, + TransactionMode.INFERRED); + this.encoding = mergedAttributes.getString("encoding"); + this.separator = getString(mergedAttributes, "separator", ScriptUtils.DEFAULT_STATEMENT_SEPARATOR); + this.commentPrefixes = getCommentPrefixes(mergedAttributes); + this.blockCommentStartDelimiter = getString(mergedAttributes, "blockCommentStartDelimiter", ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER); - this.blockCommentEndDelimiter = getString(attributes, "blockCommentEndDelimiter", + this.blockCommentEndDelimiter = getString(mergedAttributes, "blockCommentEndDelimiter", ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER); - this.errorMode = getEnum(attributes, "errorMode", ErrorMode.DEFAULT, ErrorMode.FAIL_ON_ERROR); + this.errorMode = getEnum(mergedAttributes, "errorMode", ErrorMode.DEFAULT, ErrorMode.FAIL_ON_ERROR); } /** @@ -138,10 +162,11 @@ class MergedSqlConfig { } /** - * @see SqlConfig#commentPrefix() + * @see SqlConfig#commentPrefixes() + * @since 5.2 */ - String getCommentPrefix() { - return this.commentPrefix; + String[] getCommentPrefixes() { + return this.commentPrefixes; } /** @@ -176,7 +201,7 @@ class MergedSqlConfig { .append("transactionMode", this.transactionMode) .append("encoding", this.encoding) .append("separator", this.separator) - .append("commentPrefix", this.commentPrefix) + .append("commentPrefixes", this.commentPrefixes) .append("blockCommentStartDelimiter", this.blockCommentStartDelimiter) .append("blockCommentEndDelimiter", this.blockCommentEndDelimiter) .append("errorMode", this.errorMode) @@ -202,4 +227,58 @@ class MergedSqlConfig { return value; } + private static void enforceCommentPrefixAliases(AnnotationAttributes attributes) { + String commentPrefix = attributes.getString(COMMENT_PREFIX); + String[] commentPrefixes = attributes.getStringArray(COMMENT_PREFIXES); + + boolean explicitCommentPrefix = !commentPrefix.isEmpty(); + boolean explicitCommentPrefixes = (commentPrefixes.length != 0); + Assert.isTrue(!(explicitCommentPrefix && explicitCommentPrefixes), + "You may declare the 'commentPrefix' or 'commentPrefixes' attribute in @SqlConfig but not both"); + + if (explicitCommentPrefix) { + Assert.hasText(commentPrefix, "@SqlConfig(commentPrefix) must contain text"); + attributes.put(COMMENT_PREFIXES, new String[] { commentPrefix }); + } + else if (explicitCommentPrefixes) { + for (String prefix : commentPrefixes) { + Assert.hasText(prefix, "@SqlConfig(commentPrefixes) must not contain empty prefixes"); + } + attributes.put(COMMENT_PREFIX, commentPrefixes); + } + else { + // We know commentPrefixes is an empty array, so make sure commentPrefix + // is set to that as well in order to honor the alias contract. + attributes.put(COMMENT_PREFIX, commentPrefixes); + } + } + + private static String[] getCommentPrefixes(AnnotationAttributes attributes) { + String[] commentPrefix = attributes.getStringArray(COMMENT_PREFIX); + String[] commentPrefixes = attributes.getStringArray(COMMENT_PREFIXES); + + Assert.state(Arrays.equals(commentPrefix, commentPrefixes), + "Failed to properly handle 'commentPrefix' and 'commentPrefixes' aliases"); + + return (commentPrefixes.length != 0 ? commentPrefixes : ScriptUtils.DEFAULT_COMMENT_PREFIXES); + } + + /** + * Determine if the supplied value is an explicit value (i.e., not a default). + */ + private static boolean isExplicitValue(@Nullable Object value) { + return !(isEmptyString(value) || + isEmptyArray(value) || + value == TransactionMode.DEFAULT || + value == ErrorMode.DEFAULT); + } + + private static boolean isEmptyString(@Nullable Object value) { + return (value instanceof String && ((String) value).isEmpty()); + } + + private static boolean isEmptyArray(@Nullable Object value) { + return (value != null && value.getClass().isArray() && Array.getLength(value) == 0); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java index 8b9ddf3aca..c3487bdf3b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlConfig.java @@ -42,16 +42,17 @@ import java.lang.annotation.Target; * is unfortunately not possible to assign a value of {@code null} to an annotation * attribute. Thus, in order to support overrides of inherited global * configuration, {@code @SqlConfig} attributes have an explicit - * {@code default} value of either {@code ""} for Strings or {@code DEFAULT} for - * Enums. This approach allows local declarations of {@code @SqlConfig} to - * selectively override individual attributes from global declarations of - * {@code @SqlConfig} by providing a value other than {@code ""} or {@code DEFAULT}. + * {@code default} value of either {@code ""} for Strings, {} for + * arrays, or {@code DEFAULT} for Enums. This approach allows local declarations + * of {@code @SqlConfig} to selectively override individual attributes from global + * declarations of {@code @SqlConfig} by providing a value other than {@code ""}, + * {}, or {@code DEFAULT}. * *

Inheritance and Overrides

*

Global {@code @SqlConfig} attributes are inherited whenever local * {@code @SqlConfig} attributes do not supply an explicit value other than - * {@code ""} or {@code DEFAULT}. Explicit local configuration therefore - * overrides global configuration. + * {@code ""}, {}, or {@code DEFAULT}. Explicit local configuration + * therefore overrides global configuration. * * @author Sam Brannen * @author Tadaya Tsuyukubo @@ -145,10 +146,26 @@ public @interface SqlConfig { /** * The prefix that identifies single-line comments within the SQL scripts. *

Implicitly defaults to {@code "--"}. + *

This attribute may not be used in conjunction with + * {@link #commentPrefixes commentPrefixes}, but it may be used instead of + * {@link #commentPrefixes commentPrefixes}. * @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_COMMENT_PREFIX + * @see #commentPrefixes */ String commentPrefix() default ""; + /** + * The prefixes that identify single-line comments within the SQL scripts. + *

Implicitly defaults to {@code ["--"]}. + *

This attribute may not be used in conjunction with + * {@link #commentPrefix commentPrefix}, but it may be used instead of + * {@link #commentPrefix commentPrefix}. + * @see org.springframework.jdbc.datasource.init.ScriptUtils#DEFAULT_COMMENT_PREFIXES + * @see #commentPrefix + * @since 5.2 + */ + String[] commentPrefixes() default {}; + /** * The start delimiter that identifies block comments within the SQL scripts. *

Implicitly defaults to {@code "/*"}. diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index f824a363c8..1a2ed90857 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -267,7 +267,7 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding()); populator.setSeparator(mergedSqlConfig.getSeparator()); - populator.setCommentPrefix(mergedSqlConfig.getCommentPrefix()); + populator.setCommentPrefixes(mergedSqlConfig.getCommentPrefixes()); populator.setBlockCommentStartDelimiter(mergedSqlConfig.getBlockCommentStartDelimiter()); populator.setBlockCommentEndDelimiter(mergedSqlConfig.getBlockCommentEndDelimiter()); populator.setContinueOnError(mergedSqlConfig.getErrorMode() == ErrorMode.CONTINUE_ON_ERROR); diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/SqlConfigTestInterface.java b/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/SqlConfigTestInterface.java index 968084d2e4..0011f26959 100644 --- a/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/SqlConfigTestInterface.java +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/SqlConfigTestInterface.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -27,6 +27,6 @@ import org.springframework.test.context.jdbc.SqlConfig; */ @ContextConfiguration(classes = EmptyDatabaseConfig.class) @DirtiesContext -@SqlConfig(commentPrefix = "`", blockCommentStartDelimiter = "#$", blockCommentEndDelimiter = "$#", separator = "@@") +@SqlConfig(commentPrefixes = { "`", "%%" }, blockCommentStartDelimiter = "#$", blockCommentEndDelimiter = "$#", separator = "@@") interface SqlConfigTestInterface { } diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/CustomScriptSyntaxSqlScriptsTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/CustomScriptSyntaxSqlScriptsTests.java index e15313f4cd..6bbaa3a0fc 100644 --- a/spring-test/src/test/java/org/springframework/test/context/jdbc/CustomScriptSyntaxSqlScriptsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/CustomScriptSyntaxSqlScriptsTests.java @@ -38,7 +38,7 @@ public class CustomScriptSyntaxSqlScriptsTests extends AbstractTransactionalJUni @Test @Sql("schema.sql") @Sql(scripts = "data-add-users-with-custom-script-syntax.sql",// - config = @SqlConfig(commentPrefix = "`", blockCommentStartDelimiter = "#$", blockCommentEndDelimiter = "$#", separator = "@@")) + config = @SqlConfig(commentPrefixes = { "`", "%%" }, blockCommentStartDelimiter = "#$", blockCommentEndDelimiter = "$#", separator = "@@")) public void methodLevelScripts() { assertNumUsers(3); } diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/GlobalCustomScriptSyntaxSqlScriptsTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/GlobalCustomScriptSyntaxSqlScriptsTests.java index 17038cb48d..a7eb393153 100644 --- a/spring-test/src/test/java/org/springframework/test/context/jdbc/GlobalCustomScriptSyntaxSqlScriptsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/GlobalCustomScriptSyntaxSqlScriptsTests.java @@ -33,7 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; */ @ContextConfiguration(classes = EmptyDatabaseConfig.class) @DirtiesContext -@SqlConfig(commentPrefix = "`", blockCommentStartDelimiter = "#$", blockCommentEndDelimiter = "$#", separator = "@@") +@SqlConfig(commentPrefixes = { "`", "%%" }, blockCommentStartDelimiter = "#$", blockCommentEndDelimiter = "$#", separator = "@@") public class GlobalCustomScriptSyntaxSqlScriptsTests extends AbstractTransactionalJUnit4SpringContextTests { @Test diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/MergedSqlConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/MergedSqlConfigTests.java index 10a34ed270..0180a28c76 100644 --- a/spring-test/src/test/java/org/springframework/test/context/jdbc/MergedSqlConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/MergedSqlConfigTests.java @@ -21,9 +21,11 @@ import java.lang.reflect.Method; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; 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_PREFIX; +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.test.context.jdbc.SqlConfig.ErrorMode.CONTINUE_ON_ERROR; import static org.springframework.test.context.jdbc.SqlConfig.ErrorMode.FAIL_ON_ERROR; @@ -39,17 +41,50 @@ import static org.springframework.test.context.jdbc.SqlConfig.TransactionMode.IS */ public class MergedSqlConfigTests { - private void assertDefaults(MergedSqlConfig cfg) { - assertThat(cfg).isNotNull(); - assertThat(cfg.getDataSource()).as("dataSource").isEqualTo(""); - assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo(""); - assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(INFERRED); - assertThat(cfg.getEncoding()).as("encoding").isEqualTo(""); - assertThat(cfg.getSeparator()).as("separator").isEqualTo(DEFAULT_STATEMENT_SEPARATOR); - assertThat(cfg.getCommentPrefix()).as("commentPrefix").isEqualTo(DEFAULT_COMMENT_PREFIX); - assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_START_DELIMITER); - assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_END_DELIMITER); - assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(FAIL_ON_ERROR); + @Test + public void nullLocalSqlConfig() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(null, getClass())) + .withMessage("Local @SqlConfig must not be null"); + } + + @Test + public void nullTestClass() { + SqlConfig sqlConfig = GlobalConfigClass.class.getAnnotation(SqlConfig.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(sqlConfig, null)) + .withMessage("testClass must not be null"); + } + + @Test + public void localConfigWithEmptyCommentPrefix() throws Exception { + Method method = getClass().getMethod("localConfigMethodWithEmptyCommentPrefix"); + SqlConfig sqlConfig = method.getAnnotation(Sql.class).config(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(sqlConfig, getClass())) + .withMessage("@SqlConfig(commentPrefix) must contain text"); + } + + @Test + public void localConfigWithEmptyCommentPrefixes() throws Exception { + Method method = getClass().getMethod("localConfigMethodWithEmptyCommentPrefixes"); + SqlConfig sqlConfig = method.getAnnotation(Sql.class).config(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(sqlConfig, getClass())) + .withMessage("@SqlConfig(commentPrefixes) must not contain empty prefixes"); + } + + @Test + public void localConfigWithDuplicatedCommentPrefixes() throws Exception { + Method method = getClass().getMethod("localConfigMethodWithDuplicatedCommentPrefixes"); + SqlConfig sqlConfig = method.getAnnotation(Sql.class).config(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(sqlConfig, getClass())) + .withMessage("You may declare the 'commentPrefix' or 'commentPrefixes' attribute in @SqlConfig but not both"); } @Test @@ -61,28 +96,43 @@ public class MergedSqlConfigTests { } @Test - public void globalConfigWithDefaults() throws Exception { - Method method = GlobalConfigWithDefaultsClass.class.getMethod("globalConfigMethod"); + public void localConfigWithCustomValues() throws Exception { + Method method = getClass().getMethod("localConfigMethodWithCustomValues"); SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); - MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, GlobalConfigWithDefaultsClass.class); - assertDefaults(cfg); + MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, getClass()); + + assertSoftly(softly -> { + softly.assertThat(cfg).isNotNull(); + softly.assertThat(cfg.getDataSource()).as("dataSource").isEqualTo("ds"); + softly.assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo("txMgr"); + softly.assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(ISOLATED); + softly.assertThat(cfg.getEncoding()).as("encoding").isEqualTo("enigma"); + softly.assertThat(cfg.getSeparator()).as("separator").isEqualTo("\n"); + softly.assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("`")); + softly.assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo("<<"); + softly.assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(">>"); + softly.assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(IGNORE_FAILED_DROPS); + }); } @Test - public void localConfigWithCustomValues() throws Exception { - Method method = getClass().getMethod("localConfigMethodWithCustomValues"); + public void localConfigWithCustomCommentPrefixes() throws Exception { + Method method = getClass().getMethod("localConfigMethodWithCustomCommentPrefixes"); SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, getClass()); + assertThat(cfg).isNotNull(); - assertThat(cfg.getDataSource()).as("dataSource").isEqualTo("ds"); - assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo("txMgr"); - assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(ISOLATED); - assertThat(cfg.getEncoding()).as("encoding").isEqualTo("enigma"); - assertThat(cfg.getSeparator()).as("separator").isEqualTo("\n"); - assertThat(cfg.getCommentPrefix()).as("commentPrefix").isEqualTo("`"); - assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo("<<"); - assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(">>"); - assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(IGNORE_FAILED_DROPS); + assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("`")); + } + + @Test + public void localConfigWithMultipleCommentPrefixes() throws Exception { + Method method = getClass().getMethod("localConfigMethodWithMultipleCommentPrefixes"); + SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); + MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, getClass()); + + assertThat(cfg).isNotNull(); + assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("`", "--")); } @Test @@ -90,6 +140,7 @@ public class MergedSqlConfigTests { Method method = getClass().getMethod("localConfigMethodWithContinueOnError"); SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, getClass()); + assertThat(cfg).isNotNull(); assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(CONTINUE_ON_ERROR); } @@ -99,25 +150,64 @@ public class MergedSqlConfigTests { Method method = getClass().getMethod("localConfigMethodWithIgnoreFailedDrops"); SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, getClass()); + assertThat(cfg).isNotNull(); assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(IGNORE_FAILED_DROPS); } + @Test + public void globalConfigWithEmptyCommentPrefix() throws Exception { + SqlConfig sqlConfig = GlobalConfigWithWithEmptyCommentPrefixClass.class.getAnnotation(SqlConfig.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(sqlConfig, getClass())) + .withMessage("@SqlConfig(commentPrefix) must contain text"); + } + + @Test + public void globalConfigWithEmptyCommentPrefixes() throws Exception { + SqlConfig sqlConfig = GlobalConfigWithWithEmptyCommentPrefixesClass.class.getAnnotation(SqlConfig.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(sqlConfig, getClass())) + .withMessage("@SqlConfig(commentPrefixes) must not contain empty prefixes"); + } + + @Test + public void globalConfigWithDuplicatedCommentPrefixes() throws Exception { + SqlConfig sqlConfig = GlobalConfigWithWithDuplicatedCommentPrefixesClass.class.getAnnotation(SqlConfig.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new MergedSqlConfig(sqlConfig, getClass())) + .withMessage("You may declare the 'commentPrefix' or 'commentPrefixes' attribute in @SqlConfig but not both"); + } + + @Test + public void globalConfigWithDefaults() throws Exception { + Method method = GlobalConfigWithDefaultsClass.class.getMethod("globalConfigMethod"); + SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); + MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, GlobalConfigWithDefaultsClass.class); + assertDefaults(cfg); + } + @Test public void globalConfig() throws Exception { Method method = GlobalConfigClass.class.getMethod("globalConfigMethod"); SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, GlobalConfigClass.class); - assertThat(cfg).isNotNull(); - assertThat(cfg.getDataSource()).as("dataSource").isEqualTo(""); - assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo(""); - assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(INFERRED); - assertThat(cfg.getEncoding()).as("encoding").isEqualTo("global"); - assertThat(cfg.getSeparator()).as("separator").isEqualTo("\n"); - assertThat(cfg.getCommentPrefix()).as("commentPrefix").isEqualTo(DEFAULT_COMMENT_PREFIX); - assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_START_DELIMITER); - assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_END_DELIMITER); - assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(IGNORE_FAILED_DROPS); + + assertSoftly(softly -> { + softly.assertThat(cfg).isNotNull(); + softly.assertThat(cfg.getDataSource()).as("dataSource").isEqualTo(""); + softly.assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo(""); + softly.assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(INFERRED); + softly.assertThat(cfg.getEncoding()).as("encoding").isEqualTo("global"); + softly.assertThat(cfg.getSeparator()).as("separator").isEqualTo("\n"); + softly.assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("`", "--")); + softly.assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_START_DELIMITER); + softly.assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_END_DELIMITER); + softly.assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(IGNORE_FAILED_DROPS); + }); } @Test @@ -126,20 +216,79 @@ public class MergedSqlConfigTests { SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, GlobalConfigClass.class); - assertThat(cfg).isNotNull(); - assertThat(cfg.getDataSource()).as("dataSource").isEqualTo(""); - assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo(""); - assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(INFERRED); - assertThat(cfg.getEncoding()).as("encoding").isEqualTo("local"); - assertThat(cfg.getSeparator()).as("separator").isEqualTo("@@"); - assertThat(cfg.getCommentPrefix()).as("commentPrefix").isEqualTo(DEFAULT_COMMENT_PREFIX); - assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_START_DELIMITER); - assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_END_DELIMITER); - assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(CONTINUE_ON_ERROR); + assertSoftly(softly -> { + softly.assertThat(cfg).isNotNull(); + softly.assertThat(cfg.getDataSource()).as("dataSource").isEqualTo(""); + softly.assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo(""); + softly.assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(INFERRED); + softly.assertThat(cfg.getEncoding()).as("encoding").isEqualTo("local"); + softly.assertThat(cfg.getSeparator()).as("separator").isEqualTo("@@"); + softly.assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("#")); + softly.assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_START_DELIMITER); + softly.assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_END_DELIMITER); + softly.assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(CONTINUE_ON_ERROR); + }); + } + + @Test + public void globalConfigWithCommentPrefixAndLocalOverrides() throws Exception { + Class testClass = GlobalConfigWithPrefixClass.class; + + Method method = testClass.getMethod("commentPrefixesOverrideCommentPrefix"); + SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); + MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, testClass); + + assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("#", "@")); + + method = testClass.getMethod("commentPrefixOverridesCommentPrefix"); + localSqlConfig = method.getAnnotation(Sql.class).config(); + cfg = new MergedSqlConfig(localSqlConfig, testClass); + + assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("#")); + } + + @Test + public void globalConfigWithCommentPrefixesAndLocalOverrides() throws Exception { + Class testClass = GlobalConfigWithPrefixesClass.class; + + Method method = testClass.getMethod("commentPrefixesOverrideCommentPrefixes"); + SqlConfig localSqlConfig = method.getAnnotation(Sql.class).config(); + MergedSqlConfig cfg = new MergedSqlConfig(localSqlConfig, testClass); + + assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("#", "@")); + + method = testClass.getMethod("commentPrefixOverridesCommentPrefixes"); + localSqlConfig = method.getAnnotation(Sql.class).config(); + cfg = new MergedSqlConfig(localSqlConfig, testClass); + + assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(array("#")); + } + + private void assertDefaults(MergedSqlConfig cfg) { + assertSoftly(softly -> { + softly.assertThat(cfg).isNotNull(); + softly.assertThat(cfg.getDataSource()).as("dataSource").isEqualTo(""); + softly.assertThat(cfg.getTransactionManager()).as("transactionManager").isEqualTo(""); + softly.assertThat(cfg.getTransactionMode()).as("transactionMode").isEqualTo(INFERRED); + softly.assertThat(cfg.getEncoding()).as("encoding").isEqualTo(""); + softly.assertThat(cfg.getSeparator()).as("separator").isEqualTo(DEFAULT_STATEMENT_SEPARATOR); + softly.assertThat(cfg.getCommentPrefixes()).as("commentPrefixes").isEqualTo(DEFAULT_COMMENT_PREFIXES); + softly.assertThat(cfg.getBlockCommentStartDelimiter()).as("blockCommentStartDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_START_DELIMITER); + softly.assertThat(cfg.getBlockCommentEndDelimiter()).as("blockCommentEndDelimiter").isEqualTo(DEFAULT_BLOCK_COMMENT_END_DELIMITER); + softly.assertThat(cfg.getErrorMode()).as("errorMode").isEqualTo(FAIL_ON_ERROR); + }); + } + + private static String[] array(String... elements) { + return elements; } // ------------------------------------------------------------------------- + @Sql(config = @SqlConfig(commentPrefix = "#", commentPrefixes = "#" )) + public static void localConfigMethodWithDuplicatedCommentPrefixes() { + } + @Sql public static void localConfigMethodWithDefaults() { } @@ -148,6 +297,22 @@ public class MergedSqlConfigTests { public static void localConfigMethodWithCustomValues() { } + @Sql(config = @SqlConfig(commentPrefix = " " )) + public static void localConfigMethodWithEmptyCommentPrefix() { + } + + @Sql(config = @SqlConfig(commentPrefixes = { "--", " " })) + public static void localConfigMethodWithEmptyCommentPrefixes() { + } + + @Sql(config = @SqlConfig(commentPrefixes = "`")) + public static void localConfigMethodWithCustomCommentPrefixes() { + } + + @Sql(config = @SqlConfig(commentPrefixes = { "`", "--" })) + public static void localConfigMethodWithMultipleCommentPrefixes() { + } + @Sql(config = @SqlConfig(errorMode = CONTINUE_ON_ERROR)) public static void localConfigMethodWithContinueOnError() { } @@ -156,25 +321,60 @@ public class MergedSqlConfigTests { public static void localConfigMethodWithIgnoreFailedDrops() { } + @SqlConfig(commentPrefix = " ") + public static class GlobalConfigWithWithEmptyCommentPrefixClass { + } + + @SqlConfig(commentPrefixes = { "--", " " }) + public static class GlobalConfigWithWithEmptyCommentPrefixesClass { + } + + @SqlConfig(commentPrefix = "#", commentPrefixes = "#") + public static class GlobalConfigWithWithDuplicatedCommentPrefixesClass { + } @SqlConfig public static class GlobalConfigWithDefaultsClass { - @Sql("foo.sql") + @Sql public void globalConfigMethod() { } } - @SqlConfig(encoding = "global", separator = "\n", errorMode = IGNORE_FAILED_DROPS) + @SqlConfig(encoding = "global", separator = "\n", commentPrefixes = { "`", "--" }, errorMode = IGNORE_FAILED_DROPS) public static class GlobalConfigClass { - @Sql("foo.sql") + @Sql public void globalConfigMethod() { } - @Sql(scripts = "foo.sql", config = @SqlConfig(encoding = "local", separator = "@@", errorMode = CONTINUE_ON_ERROR)) + @Sql(config = @SqlConfig(encoding = "local", separator = "@@", commentPrefix = "#", errorMode = CONTINUE_ON_ERROR)) public void globalConfigWithLocalOverridesMethod() { } } + @SqlConfig(commentPrefix = "`") + public static class GlobalConfigWithPrefixClass { + + @Sql(config = @SqlConfig(commentPrefixes = { "#", "@" })) + public void commentPrefixesOverrideCommentPrefix() { + } + + @Sql(config = @SqlConfig(commentPrefix = "#")) + public void commentPrefixOverridesCommentPrefix() { + } + } + + @SqlConfig(commentPrefixes = { "`", "--" }) + public static class GlobalConfigWithPrefixesClass { + + @Sql(config = @SqlConfig(commentPrefixes = { "#", "@" })) + public void commentPrefixesOverrideCommentPrefixes() { + } + + @Sql(config = @SqlConfig(commentPrefix = "#")) + public void commentPrefixOverridesCommentPrefixes() { + } + } + } diff --git a/spring-test/src/test/resources/org/springframework/test/context/jdbc/data-add-users-with-custom-script-syntax.sql b/spring-test/src/test/resources/org/springframework/test/context/jdbc/data-add-users-with-custom-script-syntax.sql index 664a6998e5..ec6ff77fb9 100644 --- a/spring-test/src/test/resources/org/springframework/test/context/jdbc/data-add-users-with-custom-script-syntax.sql +++ b/spring-test/src/test/resources/org/springframework/test/context/jdbc/data-add-users-with-custom-script-syntax.sql @@ -19,4 +19,7 @@ VALUES('Dilbert') INSERT INTO user VALUES('Dogbert')@@ + +%% another custom single-line comment + INSERT INTO user VALUES('Catbert')@@ diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc index d9e37f2762..4de47ea1cc 100644 --- a/src/docs/asciidoc/testing.adoc +++ b/src/docs/asciidoc/testing.adoc @@ -4190,12 +4190,12 @@ documented in the javadoc of the corresponding attribute. Due to the rules defin annotation attributes in the Java Language Specification, it is, unfortunately, not possible to assign a value of `null` to an annotation attribute. Thus, in order to support overrides of inherited global configuration, `@SqlConfig` attributes have an -explicit default value of either `""` (for Strings) or `DEFAULT` (for enumerations). This -approach lets local declarations of `@SqlConfig` selectively override individual -attributes from global declarations of `@SqlConfig` by providing a value other than `""` -or `DEFAULT`. Global `@SqlConfig` attributes are inherited whenever local `@SqlConfig` -attributes do not supply an explicit value other than `""` or `DEFAULT`. Explicit local -configuration, therefore, overrides global configuration. +explicit default value of either `""` (for Strings), `{}` (for arrays), or `DEFAULT` (for +enumerations). This approach lets local declarations of `@SqlConfig` selectively override +individual attributes from global declarations of `@SqlConfig` by providing a value other +than `""`, `{}`, or `DEFAULT`. Global `@SqlConfig` attributes are inherited whenever +local `@SqlConfig` attributes do not supply an explicit value other than `""`, `{}`, or +`DEFAULT`. Explicit local configuration, therefore, overrides global configuration. The configuration options provided by `@Sql` and `@SqlConfig` are equivalent to those supported by `ScriptUtils` and `ResourceDatabasePopulator` but are a superset of those