diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 03e84c83456..a30895a521d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -104,6 +104,7 @@ dependencies { exclude group: "commons-logging", module: "commons-logging" } optional("org.flywaydb:flyway-core") + optional("org.flywaydb:flyway-database-oracle") optional("org.flywaydb:flyway-sqlserver") optional("org.freemarker:freemarker") optional("org.glassfish.jersey.containers:jersey-container-servlet-core") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index d007fb61953..a4f15eaf61c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -24,6 +24,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; import javax.sql.DataSource; @@ -32,6 +35,8 @@ import org.flywaydb.core.api.MigrationVersion; import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.extensibility.ConfigurationExtension; +import org.flywaydb.database.oracle.OracleConfigurationExtension; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.springframework.aot.hint.RuntimeHints; @@ -61,6 +66,8 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ResourceLoader; @@ -71,6 +78,7 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; /** * {@link EnableAutoConfiguration Auto-configuration} for Flyway database migrations. @@ -116,6 +124,12 @@ public class FlywayAutoConfiguration { @EnableConfigurationProperties(FlywayProperties.class) public static class FlywayConfiguration { + private final FlywayProperties properties; + + FlywayConfiguration(FlywayProperties properties) { + this.properties = properties; + } + @Bean ResourceProviderCustomizer resourceProviderCustomizer() { return new ResourceProviderCustomizer(); @@ -123,21 +137,26 @@ public class FlywayAutoConfiguration { @Bean @ConditionalOnMissingBean(FlywayConnectionDetails.class) - PropertiesFlywayConnectionDetails flywayConnectionDetails(FlywayProperties properties) { - return new PropertiesFlywayConnectionDetails(properties); + PropertiesFlywayConnectionDetails flywayConnectionDetails() { + return new PropertiesFlywayConnectionDetails(this.properties); + } + + @Bean + @ConditionalOnClass(name = "org.flywaydb.database.oracle.OracleConfigurationExtension") + OracleFlywayConfigurationCustomizer oracleFlywayConfigurationCustomizer() { + return new OracleFlywayConfigurationCustomizer(this.properties); } @Bean - Flyway flyway(FlywayProperties properties, FlywayConnectionDetails connectionDetails, - ResourceLoader resourceLoader, ObjectProvider dataSource, - @FlywayDataSource ObjectProvider flywayDataSource, + Flyway flyway(FlywayConnectionDetails connectionDetails, ResourceLoader resourceLoader, + ObjectProvider dataSource, @FlywayDataSource ObjectProvider flywayDataSource, ObjectProvider fluentConfigurationCustomizers, ObjectProvider javaMigrations, ObjectProvider callbacks, ResourceProviderCustomizer resourceProviderCustomizer) { FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader()); configureDataSource(configuration, flywayDataSource.getIfAvailable(), dataSource.getIfUnique(), connectionDetails); - configureProperties(configuration, properties); + configureProperties(configuration, this.properties); configureCallbacks(configuration, callbacks.orderedStream().toList()); configureJavaMigrations(configuration, javaMigrations.orderedStream().toList()); fluentConfigurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); @@ -242,12 +261,6 @@ public class FlywayAutoConfiguration { map.from(properties.getDryRunOutput()).to(configuration::dryRunOutput); map.from(properties.getErrorOverrides()).to(configuration::errorOverrides); map.from(properties.getLicenseKey()).to(configuration::licenseKey); - // No method references for Oracle props for compatibility with Flyway 9.20+ - map.from(properties.getOracleSqlplus()).to((oracleSqlplus) -> configuration.oracleSqlplus(oracleSqlplus)); - map.from(properties.getOracleSqlplusWarn()) - .to((oracleSqlplusWarn) -> configuration.oracleSqlplusWarn(oracleSqlplusWarn)); - map.from(properties.getOracleKerberosCacheFile()) - .to((oracleKerberosCacheFile) -> configuration.oracleKerberosCacheFile(oracleKerberosCacheFile)); map.from(properties.getStream()).to(configuration::stream); map.from(properties.getUndoSqlMigrationPrefix()).to(configuration::undoSqlMigrationPrefix); map.from(properties.getCherryPick()).to(configuration::cherryPick); @@ -445,4 +458,54 @@ public class FlywayAutoConfiguration { } + @Order(Ordered.HIGHEST_PRECEDENCE) + static final class OracleFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { + + private final FlywayProperties properties; + + OracleFlywayConfigurationCustomizer(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public void customize(FluentConfiguration configuration) { + ConfigurationExtensionMapper map = new ConfigurationExtensionMapper<>( + PropertyMapper.get().alwaysApplyingWhenNonNull(), () -> { + OracleConfigurationExtension extension = configuration.getPluginRegister() + .getPlugin(OracleConfigurationExtension.class); + Assert.notNull(extension, "Flyway Oracle extension missing"); + return extension; + }); + map.apply(this.properties.getOracleSqlplus(), OracleConfigurationExtension::setSqlplus); + map.apply(this.properties.getOracleSqlplusWarn(), OracleConfigurationExtension::setSqlplusWarn); + map.apply(this.properties.getOracleWalletLocation(), OracleConfigurationExtension::setWalletLocation); + map.apply(this.properties.getOracleKerberosCacheFile(), OracleConfigurationExtension::setKerberosCacheFile); + } + + } + + static class ConfigurationExtensionMapper { + + private final PropertyMapper map; + + private final Supplier extensionProvider; + + ConfigurationExtensionMapper(PropertyMapper map, Supplier extensionProvider) { + this.map = map; + this.extensionProvider = SingletonSupplier.of(extensionProvider); + } + + void apply(V value, BiConsumer mapper) { + this.map.from(value).to(withExtension(mapper)); + } + + private Consumer withExtension(BiConsumer mapper) { + return (value) -> { + T extension = this.extensionProvider.get(); + mapper.accept(extension, value); + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway920AutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway91AutoConfigurationTests.java similarity index 86% rename from spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway920AutoConfigurationTests.java rename to spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway91AutoConfigurationTests.java index 770a618e472..ee7cb7ff751 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway920AutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway91AutoConfigurationTests.java @@ -23,18 +23,19 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link FlywayAutoConfiguration} with Flyway 9.20. + * Tests for {@link FlywayAutoConfiguration} with Flyway 9.19. * * @author Andy Wilkinson */ -@ClassPathOverrides({ "org.flywaydb:flyway-core:9.20.0", "org.flywaydb:flyway-sqlserver:9.20.0", - "com.h2database:h2:2.1.210" }) -class Flyway920AutoConfigurationTests { +@ClassPathExclusions("flyway-*.jar") +@ClassPathOverrides("org.flywaydb:flyway-core:9.19.4") +class Flyway91AutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index b2149465d8b..27c947d6090 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -31,8 +31,10 @@ import org.flywaydb.core.api.MigrationVersion; import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.callback.Context; import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.migration.JavaMigration; import org.flywaydb.core.internal.license.FlywayTeamsUpgradeRequiredException; +import org.flywaydb.database.oracle.OracleConfigurationExtension; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.jooq.DSLContext; @@ -49,6 +51,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.OracleFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; @@ -84,6 +87,7 @@ import org.springframework.stereotype.Component; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -602,18 +606,56 @@ class FlywayAutoConfigurationTests { + "Enterprise features, download Flyway Teams Edition & Flyway Enterprise Edition")); } + @Test + void oracleExtensionIsNotLoadedByDefault() { + FluentConfiguration configuration = mock(FluentConfiguration.class); + new OracleFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); + then(configuration).shouldHaveNoInteractions(); + } + @Test void oracleSqlplusIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-sqlplus=true") - .run(validateFlywayTeamsPropertyOnly("oracle.sqlplus")); + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getSqlplus()).isTrue()); + } @Test void oracleSqlplusWarnIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-sqlplus-warn=true") - .run(validateFlywayTeamsPropertyOnly("oracle.sqlplusWarn")); + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getSqlplusWarn()).isTrue()); + } + + @Test + void oracleWallerLocationIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle-wallet-location=/tmp/my.wallet") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getWalletLocation()).isEqualTo("/tmp/my.wallet")); + } + + @Test + void oracleKerberosCacheFileIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle-kerberos-cache-file=/tmp/cache") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getKerberosCacheFile()).isEqualTo("/tmp/cache")); } @Test @@ -683,13 +725,6 @@ class FlywayAutoConfigurationTests { .run(validateFlywayTeamsPropertyOnly("kerberosConfigFile")); } - @Test - void oracleKerberosCacheFileIsCorrectlyMapped() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.oracle-kerberos-cache-file=/tmp/cache") - .run(validateFlywayTeamsPropertyOnly("oracle.kerberosCacheFile")); - } - @Test void outputQueryResultsIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index 0c7bb4110a2..b6bca04ce2f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -109,6 +109,9 @@ class FlywayPropertiesTests { PropertyAccessorFactory.forBeanPropertyAccess(new ClassicConfiguration())); // Properties specific settings ignoreProperties(properties, "url", "driverClassName", "user", "password", "enabled"); + // Property that moved to a separate Oracle plugin + ignoreProperties(properties, "oracleSqlplus", "oracleSqlplusWarn", "oracleKerberosCacheFile", + "oracleWalletLocation"); // Property that moved to a separate SQL plugin ignoreProperties(properties, "sqlServerKerberosLoginFile"); // High level object we can't set with properties @@ -128,7 +131,7 @@ class FlywayPropertiesTests { // Handled as createSchemas ignoreProperties(configuration, "shouldCreateSchemas"); // Getters for the DataSource settings rather than actual properties - ignoreProperties(configuration, "password", "url", "user"); + ignoreProperties(configuration, "databaseType", "password", "url", "user"); // Properties not exposed by Flyway ignoreProperties(configuration, "failOnMissingTarget"); List configurationKeys = new ArrayList<>(configuration.keySet()); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4589803405b..8a55db13683 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -279,10 +279,11 @@ bom { ] } } - library("Flyway", "9.19.4") { + library("Flyway", "9.20.1") { group("org.flywaydb") { modules = [ "flyway-core", + "flyway-database-oracle", "flyway-firebird", "flyway-mysql", "flyway-sqlserver"