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 a7e93b42157c..bcb812de4f7d 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 @@ -16,28 +16,14 @@ package org.springframework.boot.autoconfigure.flyway; -import java.sql.DatabaseMetaData; -import java.time.Duration; -import java.util.Collection; import java.util.Collections; 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 javax.sql.DataSource; import org.flywaydb.core.Flyway; 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.core.internal.database.postgresql.PostgreSQLConfigurationExtension; -import org.flywaydb.database.oracle.OracleConfigurationExtension; -import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -51,37 +37,22 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition; -import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Oracle; -import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Postgresql; -import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Sqlserver; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; import org.springframework.context.annotation.Bean; 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; -import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.jdbc.support.MetaDataAccessException; -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. @@ -124,14 +95,8 @@ public FlywaySchemaManagementProvider flywayDefaultDdlModeProvider(ObjectProvide @Configuration(proxyBeanMethods = false) @ConditionalOnClass(JdbcUtils.class) @ConditionalOnMissingBean(Flyway.class) - @EnableConfigurationProperties(FlywayProperties.class) public static class FlywayConfiguration { - private final FlywayProperties properties; - - FlywayConfiguration(FlywayProperties properties) { - this.properties = properties; - } @Bean ResourceProviderCustomizer resourceProviderCustomizer() { @@ -139,272 +104,18 @@ ResourceProviderCustomizer resourceProviderCustomizer() { } @Bean - @ConditionalOnMissingBean(FlywayConnectionDetails.class) - PropertiesFlywayConnectionDetails flywayConnectionDetails() { - return new PropertiesFlywayConnectionDetails(this.properties); - } - - @Bean - @ConditionalOnClass(name = "org.flywaydb.database.sqlserver.SQLServerConfigurationExtension") - SqlServerFlywayConfigurationCustomizer sqlServerFlywayConfigurationCustomizer() { - return new SqlServerFlywayConfigurationCustomizer(this.properties); - } - - @Bean - @ConditionalOnClass(name = "org.flywaydb.database.oracle.OracleConfigurationExtension") - OracleFlywayConfigurationCustomizer oracleFlywayConfigurationCustomizer() { - return new OracleFlywayConfigurationCustomizer(this.properties); + FlywayFactory flywayFactory(ResourceLoader resourceLoader, ObjectProvider dataSource, ResourceProviderCustomizer resourceProviderCustomizer) { + return new FlywayFactory(resourceLoader, dataSource, resourceProviderCustomizer); } - @Bean - @ConditionalOnClass(name = "org.flywaydb.core.internal.database.postgresql.PostgreSQLConfigurationExtension") - PostgresqlFlywayConfigurationCustomizer postgresqlFlywayConfigurationCustomizer() { - return new PostgresqlFlywayConfigurationCustomizer(this.properties); - } - - @Bean - 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, this.properties); - configureCallbacks(configuration, callbacks.orderedStream().toList()); - configureJavaMigrations(configuration, javaMigrations.orderedStream().toList()); - fluentConfigurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); - resourceProviderCustomizer.customize(configuration); - return configuration.load(); - } - - private void configureDataSource(FluentConfiguration configuration, DataSource flywayDataSource, - DataSource dataSource, FlywayConnectionDetails connectionDetails) { - DataSource migrationDataSource = getMigrationDataSource(flywayDataSource, dataSource, connectionDetails); - configuration.dataSource(migrationDataSource); - } - - private DataSource getMigrationDataSource(DataSource flywayDataSource, DataSource dataSource, - FlywayConnectionDetails connectionDetails) { - if (flywayDataSource != null) { - return flywayDataSource; - } - String url = connectionDetails.getJdbcUrl(); - if (url != null) { - DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); - builder.url(url); - applyConnectionDetails(connectionDetails, builder); - return builder.build(); - } - String user = connectionDetails.getUsername(); - if (user != null && dataSource != null) { - DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) - .type(SimpleDriverDataSource.class); - applyConnectionDetails(connectionDetails, builder); - return builder.build(); - } - Assert.state(dataSource != null, "Flyway migration DataSource missing"); - return dataSource; - } - - private void applyConnectionDetails(FlywayConnectionDetails connectionDetails, DataSourceBuilder builder) { - builder.username(connectionDetails.getUsername()); - builder.password(connectionDetails.getPassword()); - String driverClassName = connectionDetails.getDriverClassName(); - if (StringUtils.hasText(driverClassName)) { - builder.driverClassName(driverClassName); - } - } - - /** - * Configure the given {@code configuration} using the given {@code properties}. - *

- * To maximize forwards- and backwards-compatibility method references are not - * used. - * @param configuration the configuration - * @param properties the properties - */ - private void configureProperties(FluentConfiguration configuration, FlywayProperties properties) { - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - String[] locations = new LocationResolver(configuration.getDataSource()) - .resolveLocations(properties.getLocations()) - .toArray(new String[0]); - configuration.locations(locations); - map.from(properties.isFailOnMissingLocations()) - .to((failOnMissingLocations) -> configuration.failOnMissingLocations(failOnMissingLocations)); - map.from(properties.getEncoding()).to((encoding) -> configuration.encoding(encoding)); - map.from(properties.getConnectRetries()) - .to((connectRetries) -> configuration.connectRetries(connectRetries)); - map.from(properties.getConnectRetriesInterval()) - .as(Duration::getSeconds) - .as(Long::intValue) - .to((connectRetriesInterval) -> configuration.connectRetriesInterval(connectRetriesInterval)); - map.from(properties.getLockRetryCount()) - .to((lockRetryCount) -> configuration.lockRetryCount(lockRetryCount)); - map.from(properties.getDefaultSchema()).to((schema) -> configuration.defaultSchema(schema)); - map.from(properties.getSchemas()) - .as(StringUtils::toStringArray) - .to((schemas) -> configuration.schemas(schemas)); - map.from(properties.isCreateSchemas()).to((createSchemas) -> configuration.createSchemas(createSchemas)); - map.from(properties.getTable()).to((table) -> configuration.table(table)); - map.from(properties.getTablespace()).to((tablespace) -> configuration.tablespace(tablespace)); - map.from(properties.getBaselineDescription()) - .to((baselineDescription) -> configuration.baselineDescription(baselineDescription)); - map.from(properties.getBaselineVersion()) - .to((baselineVersion) -> configuration.baselineVersion(baselineVersion)); - map.from(properties.getInstalledBy()).to((installedBy) -> configuration.installedBy(installedBy)); - map.from(properties.getPlaceholders()).to((placeholders) -> configuration.placeholders(placeholders)); - map.from(properties.getPlaceholderPrefix()) - .to((placeholderPrefix) -> configuration.placeholderPrefix(placeholderPrefix)); - map.from(properties.getPlaceholderSuffix()) - .to((placeholderSuffix) -> configuration.placeholderSuffix(placeholderSuffix)); - map.from(properties.getPlaceholderSeparator()) - .to((placeHolderSeparator) -> configuration.placeholderSeparator(placeHolderSeparator)); - map.from(properties.isPlaceholderReplacement()) - .to((placeholderReplacement) -> configuration.placeholderReplacement(placeholderReplacement)); - map.from(properties.getSqlMigrationPrefix()) - .to((sqlMigrationPrefix) -> configuration.sqlMigrationPrefix(sqlMigrationPrefix)); - map.from(properties.getSqlMigrationSuffixes()) - .as(StringUtils::toStringArray) - .to((sqlMigrationSuffixes) -> configuration.sqlMigrationSuffixes(sqlMigrationSuffixes)); - map.from(properties.getSqlMigrationSeparator()) - .to((sqlMigrationSeparator) -> configuration.sqlMigrationSeparator(sqlMigrationSeparator)); - map.from(properties.getRepeatableSqlMigrationPrefix()) - .to((repeatableSqlMigrationPrefix) -> configuration - .repeatableSqlMigrationPrefix(repeatableSqlMigrationPrefix)); - map.from(properties.getTarget()).to((target) -> configuration.target(target)); - map.from(properties.isBaselineOnMigrate()) - .to((baselineOnMigrate) -> configuration.baselineOnMigrate(baselineOnMigrate)); - map.from(properties.isCleanDisabled()).to((cleanDisabled) -> configuration.cleanDisabled(cleanDisabled)); - map.from(properties.isCleanOnValidationError()) - .to((cleanOnValidationError) -> configuration.cleanOnValidationError(cleanOnValidationError)); - map.from(properties.isGroup()).to((group) -> configuration.group(group)); - map.from(properties.isMixed()).to((mixed) -> configuration.mixed(mixed)); - map.from(properties.isOutOfOrder()).to((outOfOrder) -> configuration.outOfOrder(outOfOrder)); - map.from(properties.isSkipDefaultCallbacks()) - .to((skipDefaultCallbacks) -> configuration.skipDefaultCallbacks(skipDefaultCallbacks)); - map.from(properties.isSkipDefaultResolvers()) - .to((skipDefaultResolvers) -> configuration.skipDefaultResolvers(skipDefaultResolvers)); - map.from(properties.isValidateMigrationNaming()) - .to((validateMigrationNaming) -> configuration.validateMigrationNaming(validateMigrationNaming)); - map.from(properties.isValidateOnMigrate()) - .to((validateOnMigrate) -> configuration.validateOnMigrate(validateOnMigrate)); - map.from(properties.getInitSqls()) - .whenNot(CollectionUtils::isEmpty) - .as((initSqls) -> StringUtils.collectionToDelimitedString(initSqls, "\n")) - .to((initSql) -> configuration.initSql(initSql)); - map.from(properties.getScriptPlaceholderPrefix()) - .to((prefix) -> configuration.scriptPlaceholderPrefix(prefix)); - map.from(properties.getScriptPlaceholderSuffix()) - .to((suffix) -> configuration.scriptPlaceholderSuffix(suffix)); - configureExecuteInTransaction(configuration, properties, map); - map.from(properties::getLoggers).to((loggers) -> configuration.loggers(loggers)); - // Flyway Teams properties - map.from(properties.getBatch()).to((batch) -> configuration.batch(batch)); - map.from(properties.getDryRunOutput()).to((dryRunOutput) -> configuration.dryRunOutput(dryRunOutput)); - map.from(properties.getErrorOverrides()) - .to((errorOverrides) -> configuration.errorOverrides(errorOverrides)); - map.from(properties.getLicenseKey()).to((licenseKey) -> configuration.licenseKey(licenseKey)); - map.from(properties.getStream()).to((stream) -> configuration.stream(stream)); - map.from(properties.getUndoSqlMigrationPrefix()) - .to((undoSqlMigrationPrefix) -> configuration.undoSqlMigrationPrefix(undoSqlMigrationPrefix)); - map.from(properties.getCherryPick()).to((cherryPick) -> configuration.cherryPick(cherryPick)); - map.from(properties.getJdbcProperties()) - .whenNot(Map::isEmpty) - .to((jdbcProperties) -> configuration.jdbcProperties(jdbcProperties)); - map.from(properties.getKerberosConfigFile()) - .to((configFile) -> configuration.kerberosConfigFile(configFile)); - map.from(properties.getOutputQueryResults()) - .to((outputQueryResults) -> configuration.outputQueryResults(outputQueryResults)); - map.from(properties.getSkipExecutingMigrations()) - .to((skipExecutingMigrations) -> configuration.skipExecutingMigrations(skipExecutingMigrations)); - map.from(properties.getIgnoreMigrationPatterns()) - .whenNot(List::isEmpty) - .to((ignoreMigrationPatterns) -> configuration - .ignoreMigrationPatterns(ignoreMigrationPatterns.toArray(new String[0]))); - map.from(properties.getDetectEncoding()) - .to((detectEncoding) -> configuration.detectEncoding(detectEncoding)); - } - - private void configureExecuteInTransaction(FluentConfiguration configuration, FlywayProperties properties, - PropertyMapper map) { - try { - map.from(properties.isExecuteInTransaction()).to(configuration::executeInTransaction); - } - catch (NoSuchMethodError ex) { - // Flyway < 9.14 - } - } - - private void configureCallbacks(FluentConfiguration configuration, List callbacks) { - if (!callbacks.isEmpty()) { - configuration.callbacks(callbacks.toArray(new Callback[0])); - } - } - - private void configureJavaMigrations(FluentConfiguration flyway, List migrations) { - if (!migrations.isEmpty()) { - flyway.javaMigrations(migrations.toArray(new JavaMigration[0])); - } - } - - @Bean - @ConditionalOnMissingBean - public FlywayMigrationInitializer flywayInitializer(Flyway flyway, - ObjectProvider migrationStrategy) { - return new FlywayMigrationInitializer(flyway, migrationStrategy.getIfAvailable()); + @Configuration(proxyBeanMethods = false) + @Import(FlywayInstancesRegistrar.class) + static class FlywayInstancesConfiguration { } } - private static class LocationResolver { - - private static final String VENDOR_PLACEHOLDER = "{vendor}"; - - private final DataSource dataSource; - - LocationResolver(DataSource dataSource) { - this.dataSource = dataSource; - } - - List resolveLocations(List locations) { - if (usesVendorLocation(locations)) { - DatabaseDriver databaseDriver = getDatabaseDriver(); - return replaceVendorLocations(locations, databaseDriver); - } - return locations; - } - - private List replaceVendorLocations(List locations, DatabaseDriver databaseDriver) { - if (databaseDriver == DatabaseDriver.UNKNOWN) { - return locations; - } - String vendor = databaseDriver.getId(); - return locations.stream().map((location) -> location.replace(VENDOR_PLACEHOLDER, vendor)).toList(); - } - - private DatabaseDriver getDatabaseDriver() { - try { - String url = JdbcUtils.extractDatabaseMetaData(this.dataSource, DatabaseMetaData::getURL); - return DatabaseDriver.fromJdbcUrl(url); - } - catch (MetaDataAccessException ex) { - throw new IllegalStateException(ex); - } - } - - private boolean usesVendorLocation(Collection locations) { - for (String location : locations) { - if (location.contains(VENDOR_PLACEHOLDER)) { - return true; - } - } - return false; - } - - } /** * Convert a String or Number to a {@link MigrationVersion}. @@ -498,98 +209,4 @@ public String getDriverClassName() { } - @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) { - Extension extension = new Extension<>(configuration, - OracleConfigurationExtension.class, "Oracle"); - Oracle properties = this.properties.getOracle(); - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties::getSqlplus).to(extension.via((ext, sqlplus) -> ext.setSqlplus(sqlplus))); - map.from(properties::getSqlplusWarn) - .to(extension.via((ext, sqlplusWarn) -> ext.setSqlplusWarn(sqlplusWarn))); - map.from(properties::getWalletLocation) - .to(extension.via((ext, walletLocation) -> ext.setWalletLocation(walletLocation))); - map.from(properties::getKerberosCacheFile) - .to(extension.via((ext, kerberosCacheFile) -> ext.setKerberosCacheFile(kerberosCacheFile))); - } - - } - - @Order(Ordered.HIGHEST_PRECEDENCE) - static final class PostgresqlFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { - - private final FlywayProperties properties; - - PostgresqlFlywayConfigurationCustomizer(FlywayProperties properties) { - this.properties = properties; - } - - @Override - public void customize(FluentConfiguration configuration) { - Extension extension = new Extension<>(configuration, - PostgreSQLConfigurationExtension.class, "PostgreSQL"); - Postgresql properties = this.properties.getPostgresql(); - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties::getTransactionalLock) - .to(extension.via((ext, transactionalLock) -> ext.setTransactionalLock(transactionalLock))); - } - - } - - @Order(Ordered.HIGHEST_PRECEDENCE) - static final class SqlServerFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { - - private final FlywayProperties properties; - - SqlServerFlywayConfigurationCustomizer(FlywayProperties properties) { - this.properties = properties; - } - - @Override - public void customize(FluentConfiguration configuration) { - Extension extension = new Extension<>(configuration, - SQLServerConfigurationExtension.class, "SQL Server"); - Sqlserver properties = this.properties.getSqlserver(); - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties::getKerberosLoginFile).to(extension.via(this::setKerberosLoginFile)); - } - - private void setKerberosLoginFile(SQLServerConfigurationExtension configuration, String file) { - configuration.getKerberos().getLogin().setFile(file); - } - - } - - /** - * Helper class used to map properties to a {@link ConfigurationExtension}. - * - * @param the extension type - */ - static class Extension { - - private SingletonSupplier extension; - - Extension(FluentConfiguration configuration, Class type, String name) { - this.extension = SingletonSupplier.of(() -> { - E extension = configuration.getPluginRegister().getPlugin(type); - Assert.notNull(extension, () -> "Flyway %s extension missing".formatted(name)); - return extension; - }); - } - - Consumer via(BiConsumer action) { - return (value) -> action.accept(this.extension.get(), value); - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayFactory.java new file mode 100644 index 000000000000..7cc5a7275e62 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayFactory.java @@ -0,0 +1,421 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.sql.DatabaseMetaData; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import javax.sql.DataSource; + +import org.flywaydb.core.Flyway; +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.core.internal.database.postgresql.PostgreSQLConfigurationExtension; +import org.flywaydb.database.oracle.OracleConfigurationExtension; +import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.PropertiesFlywayConnectionDetails; +import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Oracle; +import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Postgresql; +import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Sqlserver; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; + +class FlywayFactory { + + private final ResourceLoader resourceLoader; + + private final ObjectProvider dataSource; + + private final ResourceProviderCustomizer resourceProviderCustomizer; + + FlywayFactory(ResourceLoader resourceLoader, ObjectProvider dataSource, ResourceProviderCustomizer resourceProviderCustomizer) { + this.resourceLoader = resourceLoader; + this.dataSource = dataSource; + this.resourceProviderCustomizer = resourceProviderCustomizer; + } + + Flyway createFlyway(FlywayProperties properties, + ObjectProvider connectionDetails, + ObjectProvider flywayDataSource, + ObjectProvider flywayConfigurationCustomizers, + ObjectProvider callbacks, + ObjectProvider javaMigrations) { + FluentConfiguration configuration = new FluentConfiguration(this.resourceLoader.getClassLoader()); + configureDataSource(configuration, flywayDataSource.getIfAvailable(), this.dataSource.getIfUnique(), + connectionDetails.getIfAvailable(() -> new PropertiesFlywayConnectionDetails(properties))); + configureProperties(configuration, properties); + configureCallbacks(configuration, callbacks.orderedStream().toList()); + configureJavaMigrations(configuration, javaMigrations.orderedStream().toList()); + createDatabaseCustomizers(properties).forEach((customizer) -> customizer.customize(configuration)); + flywayConfigurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); + this.resourceProviderCustomizer.customize(configuration); + return configuration.load(); + } + + private void configureDataSource(FluentConfiguration configuration, DataSource flywayDataSource, + DataSource dataSource, FlywayConnectionDetails connectionDetails) { + DataSource migrationDataSource = getMigrationDataSource(flywayDataSource, dataSource, connectionDetails); + configuration.dataSource(migrationDataSource); + } + + private DataSource getMigrationDataSource(DataSource flywayDataSource, DataSource dataSource, + FlywayConnectionDetails connectionDetails) { + if (flywayDataSource != null) { + return flywayDataSource; + } + String url = connectionDetails.getJdbcUrl(); + if (url != null) { + DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); + builder.url(url); + applyConnectionDetails(connectionDetails, builder); + return builder.build(); + } + String user = connectionDetails.getUsername(); + if (user != null && dataSource != null) { + DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) + .type(SimpleDriverDataSource.class); + applyConnectionDetails(connectionDetails, builder); + return builder.build(); + } + Assert.state(dataSource != null, "Flyway migration DataSource missing"); + return dataSource; + } + + private void applyConnectionDetails(FlywayConnectionDetails connectionDetails, DataSourceBuilder builder) { + builder.username(connectionDetails.getUsername()); + builder.password(connectionDetails.getPassword()); + String driverClassName = connectionDetails.getDriverClassName(); + if (StringUtils.hasText(driverClassName)) { + builder.driverClassName(driverClassName); + } + } + + /** + * Configure the given {@code configuration} using the given {@code properties}. + *

+ * To maximize forwards- and backwards-compatibility method references are not + * used. + * @param configuration the configuration + * @param properties the properties + */ + private void configureProperties(FluentConfiguration configuration, FlywayProperties properties) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + String[] locations = new LocationResolver(configuration.getDataSource()) + .resolveLocations(properties.getLocations()) + .toArray(new String[0]); + configuration.locations(locations); + map.from(properties.isFailOnMissingLocations()) + .to((failOnMissingLocations) -> configuration.failOnMissingLocations(failOnMissingLocations)); + map.from(properties.getEncoding()).to((encoding) -> configuration.encoding(encoding)); + map.from(properties.getConnectRetries()) + .to((connectRetries) -> configuration.connectRetries(connectRetries)); + map.from(properties.getConnectRetriesInterval()) + .as(Duration::getSeconds) + .as(Long::intValue) + .to((connectRetriesInterval) -> configuration.connectRetriesInterval(connectRetriesInterval)); + map.from(properties.getLockRetryCount()) + .to((lockRetryCount) -> configuration.lockRetryCount(lockRetryCount)); + map.from(properties.getDefaultSchema()).to((schema) -> configuration.defaultSchema(schema)); + map.from(properties.getSchemas()) + .as(StringUtils::toStringArray) + .to((schemas) -> configuration.schemas(schemas)); + map.from(properties.isCreateSchemas()).to((createSchemas) -> configuration.createSchemas(createSchemas)); + map.from(properties.getTable()).to((table) -> configuration.table(table)); + map.from(properties.getTablespace()).to((tablespace) -> configuration.tablespace(tablespace)); + map.from(properties.getBaselineDescription()) + .to((baselineDescription) -> configuration.baselineDescription(baselineDescription)); + map.from(properties.getBaselineVersion()) + .to((baselineVersion) -> configuration.baselineVersion(baselineVersion)); + map.from(properties.getInstalledBy()).to((installedBy) -> configuration.installedBy(installedBy)); + map.from(properties.getPlaceholders()).to((placeholders) -> configuration.placeholders(placeholders)); + map.from(properties.getPlaceholderPrefix()) + .to((placeholderPrefix) -> configuration.placeholderPrefix(placeholderPrefix)); + map.from(properties.getPlaceholderSuffix()) + .to((placeholderSuffix) -> configuration.placeholderSuffix(placeholderSuffix)); + map.from(properties.getPlaceholderSeparator()) + .to((placeHolderSeparator) -> configuration.placeholderSeparator(placeHolderSeparator)); + map.from(properties.isPlaceholderReplacement()) + .to((placeholderReplacement) -> configuration.placeholderReplacement(placeholderReplacement)); + map.from(properties.getSqlMigrationPrefix()) + .to((sqlMigrationPrefix) -> configuration.sqlMigrationPrefix(sqlMigrationPrefix)); + map.from(properties.getSqlMigrationSuffixes()) + .as(StringUtils::toStringArray) + .to((sqlMigrationSuffixes) -> configuration.sqlMigrationSuffixes(sqlMigrationSuffixes)); + map.from(properties.getSqlMigrationSeparator()) + .to((sqlMigrationSeparator) -> configuration.sqlMigrationSeparator(sqlMigrationSeparator)); + map.from(properties.getRepeatableSqlMigrationPrefix()) + .to((repeatableSqlMigrationPrefix) -> configuration + .repeatableSqlMigrationPrefix(repeatableSqlMigrationPrefix)); + map.from(properties.getTarget()).to((target) -> configuration.target(target)); + map.from(properties.isBaselineOnMigrate()) + .to((baselineOnMigrate) -> configuration.baselineOnMigrate(baselineOnMigrate)); + map.from(properties.isCleanDisabled()).to((cleanDisabled) -> configuration.cleanDisabled(cleanDisabled)); + map.from(properties.isCleanOnValidationError()) + .to((cleanOnValidationError) -> configuration.cleanOnValidationError(cleanOnValidationError)); + map.from(properties.isGroup()).to((group) -> configuration.group(group)); + map.from(properties.isMixed()).to((mixed) -> configuration.mixed(mixed)); + map.from(properties.isOutOfOrder()).to((outOfOrder) -> configuration.outOfOrder(outOfOrder)); + map.from(properties.isSkipDefaultCallbacks()) + .to((skipDefaultCallbacks) -> configuration.skipDefaultCallbacks(skipDefaultCallbacks)); + map.from(properties.isSkipDefaultResolvers()) + .to((skipDefaultResolvers) -> configuration.skipDefaultResolvers(skipDefaultResolvers)); + map.from(properties.isValidateMigrationNaming()) + .to((validateMigrationNaming) -> configuration.validateMigrationNaming(validateMigrationNaming)); + map.from(properties.isValidateOnMigrate()) + .to((validateOnMigrate) -> configuration.validateOnMigrate(validateOnMigrate)); + map.from(properties.getInitSqls()) + .whenNot(CollectionUtils::isEmpty) + .as((initSqls) -> StringUtils.collectionToDelimitedString(initSqls, "\n")) + .to((initSql) -> configuration.initSql(initSql)); + map.from(properties.getScriptPlaceholderPrefix()) + .to((prefix) -> configuration.scriptPlaceholderPrefix(prefix)); + map.from(properties.getScriptPlaceholderSuffix()) + .to((suffix) -> configuration.scriptPlaceholderSuffix(suffix)); + configureExecuteInTransaction(configuration, properties, map); + map.from(properties::getLoggers).to((loggers) -> configuration.loggers(loggers)); + // Flyway Teams properties + map.from(properties.getBatch()).to((batch) -> configuration.batch(batch)); + map.from(properties.getDryRunOutput()).to((dryRunOutput) -> configuration.dryRunOutput(dryRunOutput)); + map.from(properties.getErrorOverrides()) + .to((errorOverrides) -> configuration.errorOverrides(errorOverrides)); + map.from(properties.getLicenseKey()).to((licenseKey) -> configuration.licenseKey(licenseKey)); + map.from(properties.getStream()).to((stream) -> configuration.stream(stream)); + map.from(properties.getUndoSqlMigrationPrefix()) + .to((undoSqlMigrationPrefix) -> configuration.undoSqlMigrationPrefix(undoSqlMigrationPrefix)); + map.from(properties.getCherryPick()).to((cherryPick) -> configuration.cherryPick(cherryPick)); + map.from(properties.getJdbcProperties()) + .whenNot(Map::isEmpty) + .to((jdbcProperties) -> configuration.jdbcProperties(jdbcProperties)); + map.from(properties.getKerberosConfigFile()) + .to((configFile) -> configuration.kerberosConfigFile(configFile)); + map.from(properties.getOutputQueryResults()) + .to((outputQueryResults) -> configuration.outputQueryResults(outputQueryResults)); + map.from(properties.getSkipExecutingMigrations()) + .to((skipExecutingMigrations) -> configuration.skipExecutingMigrations(skipExecutingMigrations)); + map.from(properties.getIgnoreMigrationPatterns()) + .whenNot(List::isEmpty) + .to((ignoreMigrationPatterns) -> configuration + .ignoreMigrationPatterns(ignoreMigrationPatterns.toArray(new String[0]))); + map.from(properties.getDetectEncoding()) + .to((detectEncoding) -> configuration.detectEncoding(detectEncoding)); + } + + private void configureExecuteInTransaction(FluentConfiguration configuration, FlywayProperties properties, + PropertyMapper map) { + try { + map.from(properties.isExecuteInTransaction()).to(configuration::executeInTransaction); + } + catch (NoSuchMethodError ex) { + // Flyway < 9.14 + } + } + + private void configureCallbacks(FluentConfiguration configuration, List callbacks) { + if (!callbacks.isEmpty()) { + configuration.callbacks(callbacks.toArray(new Callback[0])); + } + } + + private void configureJavaMigrations(FluentConfiguration flyway, List migrations) { + if (!migrations.isEmpty()) { + flyway.javaMigrations(migrations.toArray(new JavaMigration[0])); + } + } + + private static class LocationResolver { + + private static final String VENDOR_PLACEHOLDER = "{vendor}"; + + private final DataSource dataSource; + + LocationResolver(DataSource dataSource) { + this.dataSource = dataSource; + } + + List resolveLocations(List locations) { + if (usesVendorLocation(locations)) { + DatabaseDriver databaseDriver = getDatabaseDriver(); + return replaceVendorLocations(locations, databaseDriver); + } + return locations; + } + + private List replaceVendorLocations(List locations, DatabaseDriver databaseDriver) { + if (databaseDriver == DatabaseDriver.UNKNOWN) { + return locations; + } + String vendor = databaseDriver.getId(); + return locations.stream().map((location) -> location.replace(VENDOR_PLACEHOLDER, vendor)).toList(); + } + + private DatabaseDriver getDatabaseDriver() { + try { + String url = JdbcUtils.extractDatabaseMetaData(this.dataSource, DatabaseMetaData::getURL); + return DatabaseDriver.fromJdbcUrl(url); + } + catch (MetaDataAccessException ex) { + throw new IllegalStateException(ex); + } + + } + + private boolean usesVendorLocation(Collection locations) { + for (String location : locations) { + if (location.contains(VENDOR_PLACEHOLDER)) { + return true; + } + } + return false; + } + + } + + private List createDatabaseCustomizers(FlywayProperties properties) { + List customizers = new ArrayList<>(); + if (isClassLoadable("org.flywaydb.database.sqlserver.SQLServerConfigurationExtension")) { + customizers.add(new SqlServerFlywayConfigurationCustomizer(properties)); + } + if (isClassLoadable("org.flywaydb.database.oracle.OracleConfigurationExtension")) { + customizers.add(new OracleFlywayConfigurationCustomizer(properties)); + } + if (isClassLoadable("org.flywaydb.core.internal.database.postgresql.PostgreSQLConfigurationExtension")) { + customizers.add(new PostgresqlFlywayConfigurationCustomizer(properties)); + } + return customizers; + } + + private boolean isClassLoadable(String className) { + try { + Class.forName(className, false, getClass().getClassLoader()); + return true; + } + catch (Throwable ex) { + return false; + } + } + + static final class OracleFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { + + private final FlywayProperties properties; + + OracleFlywayConfigurationCustomizer(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public void customize(FluentConfiguration configuration) { + Extension extension = new Extension<>(configuration, + OracleConfigurationExtension.class, "Oracle"); + Oracle properties = this.properties.getOracle(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getSqlplus).to(extension.via((ext, sqlplus) -> ext.setSqlplus(sqlplus))); + map.from(properties::getSqlplusWarn) + .to(extension.via((ext, sqlplusWarn) -> ext.setSqlplusWarn(sqlplusWarn))); + map.from(properties::getWalletLocation) + .to(extension.via((ext, walletLocation) -> ext.setWalletLocation(walletLocation))); + map.from(properties::getKerberosCacheFile) + .to(extension.via((ext, kerberosCacheFile) -> ext.setKerberosCacheFile(kerberosCacheFile))); + } + + } + + static final class PostgresqlFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { + + private final FlywayProperties properties; + + PostgresqlFlywayConfigurationCustomizer(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public void customize(FluentConfiguration configuration) { + Extension extension = new Extension<>(configuration, + PostgreSQLConfigurationExtension.class, "PostgreSQL"); + Postgresql properties = this.properties.getPostgresql(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getTransactionalLock) + .to(extension.via((ext, transactionalLock) -> ext.setTransactionalLock(transactionalLock))); + } + + } + + static final class SqlServerFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { + + private final FlywayProperties properties; + + SqlServerFlywayConfigurationCustomizer(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public void customize(FluentConfiguration configuration) { + Extension extension = new Extension<>(configuration, + SQLServerConfigurationExtension.class, "SQL Server"); + Sqlserver properties = this.properties.getSqlserver(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getKerberosLoginFile).to(extension.via(this::setKerberosLoginFile)); + } + + private void setKerberosLoginFile(SQLServerConfigurationExtension configuration, String file) { + configuration.getKerberos().getLogin().setFile(file); + } + + } + + /** + * Helper class used to map properties to a {@link ConfigurationExtension}. + * + * @param the extension type + */ + static class Extension { + + private SingletonSupplier extension; + + Extension(FluentConfiguration configuration, Class type, String name) { + this.extension = SingletonSupplier.of(() -> { + E extension = configuration.getPluginRegister().getPlugin(type); + Assert.notNull(extension, () -> "Flyway %s extension missing".formatted(name)); + return extension; + }); + } + + Consumer via(BiConsumer action) { + return (value) -> action.accept(this.extension.get(), value); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstance.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstance.java new file mode 100644 index 000000000000..06e2eead6a4f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstance.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE,ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FlywayInstance { + + String value() default ""; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstancesProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstancesProperties.java new file mode 100644 index 000000000000..ac5a4e137c91 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstancesProperties.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "spring.flyway") +public class FlywayInstancesProperties extends FlywayProperties { + + private Map instances = new HashMap<>(); + + public Map getInstances() { + return this.instances; + } + + public void setInstances(Map instances) { + this.instances = instances; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstancesRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstancesRegistrar.java new file mode 100644 index 000000000000..9d488132b565 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayInstancesRegistrar.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.migration.JavaMigration; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; + +class FlywayInstancesRegistrar implements ImportBeanDefinitionRegistrar { + private final ListableBeanFactory listableBeanFactory; + private final Binder binder; + + public FlywayInstancesRegistrar(BeanFactory beanFactory, Environment environment) { + this.listableBeanFactory = (ListableBeanFactory) beanFactory; + this.binder = Binder.get(environment); + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + FlywayInstancesProperties instancesProperties = new FlywayInstancesProperties(); + this.binder.bind("spring.flyway", Bindable.ofInstance(instancesProperties)); + + if (instancesProperties.getInstances().isEmpty()) { + registry.registerBeanDefinition("flyway", + BeanDefinitionBuilder.genericBeanDefinition(Flyway.class, createFlywaySupplier(null, instancesProperties)).getBeanDefinition()); + + if (this.listableBeanFactory.getBeanNamesForType(FlywayMigrationInitializer.class).length == 0) { + registry.registerBeanDefinition("flywayMigrationInitializer", + BeanDefinitionBuilder.genericBeanDefinition(FlywayMigrationInitializer.class, + createFlywayMigrationInitializerSupplier(null, "flyway")).getBeanDefinition()); + } + } else { + for (String flywayInstanceName : instancesProperties.getInstances().keySet()) { + FlywayProperties properties = bindFlywayInstanceProperties(flywayInstanceName); + + if (properties.isEnabled()) { + String flywayBeanName = "flyway-" + flywayInstanceName; + + registry.registerBeanDefinition(flywayBeanName, + BeanDefinitionBuilder.genericBeanDefinition(Flyway.class, createFlywaySupplier(flywayInstanceName, properties)).getBeanDefinition()); + + registry.registerBeanDefinition("flywayMigrationInitializer-" + flywayInstanceName, + BeanDefinitionBuilder.genericBeanDefinition(FlywayMigrationInitializer.class, + createFlywayMigrationInitializerSupplier(flywayInstanceName, flywayBeanName)).getBeanDefinition()); + } + } + } + } + + private FlywayProperties bindFlywayInstanceProperties(String flywayInstanceName) { + FlywayProperties properties = new FlywayProperties(); + this.binder.bind("spring.flyway", Bindable.ofInstance(properties)); + this.binder.bind("spring.flyway.instances." + flywayInstanceName, Bindable.ofInstance(properties)); + return properties; + } + + private Supplier createFlywaySupplier(String flywayInstanceName, FlywayProperties properties) { + return () -> { + ObjectProvider connectionDetails = findBeans(flywayInstanceName, FlywayConnectionDetails.class, null, true, true); + ObjectProvider flywayDataSource = findBeans(flywayInstanceName, DataSource.class, FlywayDataSource.class, true, true); + ObjectProvider flywayConfigurationCustomizers = findBeans(flywayInstanceName, FlywayConfigurationCustomizer.class, null, false, true); + ObjectProvider callbacks = findBeans(flywayInstanceName, Callback.class, null, false, true); + ObjectProvider javaMigrations = findBeans(flywayInstanceName, JavaMigration.class, null, true, false); + + FlywayFactory flywayFactory = this.listableBeanFactory.getBean(FlywayFactory.class); + return flywayFactory.createFlyway(properties, connectionDetails, flywayDataSource, flywayConfigurationCustomizers, callbacks, javaMigrations); + }; + } + + private Supplier createFlywayMigrationInitializerSupplier(String flywayInstanceName, String flywayBeanName) { + return () -> { + ObjectProvider flywayMigrationStrategy = findBeans(flywayInstanceName, FlywayMigrationStrategy.class, null, true, true); + return new FlywayMigrationInitializer(this.listableBeanFactory.getBean(flywayBeanName, Flyway.class), flywayMigrationStrategy.getIfAvailable()); + }; + } + + private ObjectProvider findBeans(String flywayInstanceName, Class type, Class additionalAnnotation, boolean preferInstanceBeans, boolean allowGeneralBeans) { + if (flywayInstanceName == null) { + return this.listableBeanFactory.getBeanProvider(type); + } + + Map generalBeans = new HashMap<>(); + Map instanceBeans = new HashMap<>(); + + for (Map.Entry beanWithName : this.listableBeanFactory.getBeansOfType(type).entrySet()) { + if (additionalAnnotation != null && this.listableBeanFactory.findAnnotationOnBean(beanWithName.getKey(), additionalAnnotation) == null) { + continue; + } + + FlywayInstance flywayInstanceSpecific = this.listableBeanFactory.findAnnotationOnBean(beanWithName.getKey(), FlywayInstance.class); + if (flywayInstanceSpecific == null) { + Assert.isTrue(allowGeneralBeans, "Undefined instance binding for bean " + beanWithName.getKey() + " of type " + type.getName()); + + generalBeans.put(beanWithName.getKey(), beanWithName.getValue()); + } else if (flywayInstanceSpecific.value().equals(flywayInstanceName)) { + instanceBeans.put(beanWithName.getKey(), beanWithName.getValue()); + } + } + + Stream> stream; + if (preferInstanceBeans && !instanceBeans.isEmpty()) { + stream = Stream.of(instanceBeans); + } else { + stream = Stream.of(generalBeans, instanceBeans); + } + + StaticListableBeanFactory staticListableBeanFactory = new StaticListableBeanFactory(); + stream.flatMap(map -> map.entrySet().stream()).forEach(beanWithName -> staticListableBeanFactory.addBean(beanWithName.getKey(), beanWithName.getValue())); + return staticListableBeanFactory.getBeanProvider(type); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java index 4a6d7db6e0ca..aad616543488 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.convert.DurationUnit; @@ -40,7 +39,6 @@ * @author Chris Bono * @since 1.1.0 */ -@ConfigurationProperties(prefix = "spring.flyway") public class FlywayProperties { /** 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 c462bc02716a..cc8574418566 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 @@ -18,20 +18,23 @@ import java.sql.Connection; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.BiConsumer; import javax.sql.DataSource; +import com.zaxxer.hikari.HikariDataSource; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.Location; 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.database.postgresql.PostgreSQLConfigurationExtension; import org.flywaydb.core.internal.license.FlywayTeamsUpgradeRequiredException; @@ -41,6 +44,7 @@ import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.impl.DefaultDSLContext; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; @@ -52,9 +56,6 @@ 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.flyway.FlywayAutoConfiguration.PostgresqlFlywayConfigurationCustomizer; -import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.SqlServerFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; @@ -90,7 +91,6 @@ 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; @@ -112,6 +112,253 @@ @ExtendWith(OutputCaptureExtension.class) class FlywayAutoConfigurationTests { + @Nested + class FlywayInstanceTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) + .withPropertyValues("spring.datasource.generate-unique-name=true") + .withPropertyValues("spring.flyway.instances.schema1.enabled=true") + .withPropertyValues("spring.flyway.instances.schema2.enabled=true"); + + @Test + void testFlywayConnectionDetails() { + BiConsumer assertFlywayConnectionDetails = (flyway, instance) -> { + DataSource dataSource = flyway.getConfiguration().getDataSource(); + assertThat(dataSource).isInstanceOf(SimpleDriverDataSource.class); + SimpleDriverDataSource simpleDriverDataSource = (SimpleDriverDataSource) dataSource; + assertThat(simpleDriverDataSource.getUrl()) + .isEqualTo("jdbc:postgresql://database.example.com:12345/database-" + instance); + assertThat(simpleDriverDataSource.getUsername()).isEqualTo("user-" + instance); + assertThat(simpleDriverDataSource.getPassword()).isEqualTo("secret-" + instance); + assertThat(simpleDriverDataSource.getDriver()).isInstanceOf(Driver.class); + }; + + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, FlywayConnectionDetailsConfiguration.class, + MockFlywayMigrationStrategy.class) + .run((context) -> { + assertFlywayConnectionDetails.accept(context.getBean("flyway-schema1", Flyway.class), "global"); + assertFlywayConnectionDetails.accept(context.getBean("flyway-schema2", Flyway.class), "schema2"); + }); + } + + @Test + void testFlywayDataSources() { + BiConsumer assertFlywayDataSource = (flyway, instance) -> { + DataSource dataSource = flyway.getConfiguration().getDataSource(); + assertThat(((HikariDataSource) dataSource).getJdbcUrl()) + .isEqualTo("jdbc:hsqldb:mem:" + instance); + }; + + this.contextRunner + .withUserConfiguration(FlywayDataSourceConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + assertFlywayDataSource.accept(context.getBean("flyway-schema1", Flyway.class), "global"); + assertFlywayDataSource.accept(context.getBean("flyway-schema2", Flyway.class), "schema2"); + }); + } + + @Test + void testFlywayConfigurationCustomizers() { + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, ConfigurationCustomizerConfiguration.class) + .run((context) -> { + Flyway flywaySchema1 = context.getBean("flyway-schema1", Flyway.class); + assertThat(flywaySchema1.getConfiguration().getConnectRetries()).isEqualTo(10); + assertThat(flywaySchema1.getConfiguration().getBaselineDescription()).isEqualTo("<< Global baseline >>"); + + Flyway flywaySchema2 = context.getBean("flyway-schema2", Flyway.class); + assertThat(flywaySchema2.getConfiguration().getConnectRetries()).isEqualTo(5); + assertThat(flywaySchema2.getConfiguration().getBaselineDescription()).isEqualTo("<< Custom baseline schema2 >>"); + assertThat(flywaySchema2.getConfiguration().getPlaceholders()).containsEntry("instance", "schema2-overwrite"); + }); + } + + @Test + void testCallbacks() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, CallbackConfiguration.class) + .run((context) -> { + Flyway flywaySchema1 = context.getBean("flyway-schema1", Flyway.class); + assertThat(flywaySchema1.getConfiguration().getCallbacks()).hasSize(1); + + Flyway flywaySchema2 = context.getBean("flyway-schema2", Flyway.class); + assertThat(flywaySchema2.getConfiguration().getCallbacks()).hasSize(3); + + Callback globalCallback = context.getBean("globalCallback", Callback.class); + Callback schema2CallbackOne = context.getBean("schema2CallbackOne", Callback.class); + Callback schema2CallbackTwo = context.getBean("schema2CallbackTwo", Callback.class); + InOrder orderedCallbacks = inOrder(globalCallback, schema2CallbackOne, schema2CallbackTwo); + + orderedCallbacks.verify(globalCallback).handle(any(Event.class), any(Context.class)); + orderedCallbacks.verify(schema2CallbackTwo).handle(any(Event.class), any(Context.class)); + orderedCallbacks.verify(schema2CallbackOne).handle(any(Event.class), any(Context.class)); + }); + } + + @Test + void flywayJavaMigrations() { + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, FlywayJavaMigrationsConfiguration.class) + .run((context) -> { + Flyway flywaySchema1 = context.getBean("flyway-schema1", Flyway.class); + assertThat(flywaySchema1.getConfiguration().getJavaMigrations()).isEmpty(); + + Flyway flywaySchema2 = context.getBean("flyway-schema2", Flyway.class); + assertThat(flywaySchema2.getConfiguration().getJavaMigrations()).hasSize(1); + JavaMigration schema2Migration = context.getBean("schema2Migration", JavaMigration.class); + assertThat(flywaySchema2.getConfiguration().getJavaMigrations()).containsExactly(schema2Migration); + }); + } + + @Test + void testFlywayMigrationStrategy() { + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, Schema2FlywayMigrationStrategy.class) + .withPropertyValues("spring.flyway.instances.schema2.defaultSchema=SCHEMA2") + .run((context) -> { + assertThat(context.getBean(Schema2FlywayMigrationStrategy.class).getDefaultSchemas()).containsExactly("SCHEMA2"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class FlywayConnectionDetailsConfiguration { + + @Bean + FlywayConnectionDetails globalFlywayConnectionDetails() { + return createFlywayConnectionDetails("global"); + } + + @Bean + @FlywayInstance("schema2") + FlywayConnectionDetails schema2FlywayConnectionDetails() { + return createFlywayConnectionDetails("schema2"); + } + + private static FlywayConnectionDetails createFlywayConnectionDetails(String instance) { + return new FlywayConnectionDetails() { + + @Override + public String getJdbcUrl() { + return "jdbc:postgresql://database.example.com:12345/database-" + instance; + } + + @Override + public String getUsername() { + return "user-" + instance; + } + + @Override + public String getPassword() { + return "secret-" + instance; + } + + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class FlywayDataSourceConfiguration { + + @FlywayDataSource + @Bean + DataSource globalFlywayDataSource() { + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:global").username("sa").build(); + } + + @FlywayDataSource + @Bean + @FlywayInstance("schema2") + DataSource schema2flywayDataSource() { + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:schema2").username("sa").build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ConfigurationCustomizerConfiguration { + + @Bean + @Order(2) + @FlywayInstance("schema2") + FlywayConfigurationCustomizer customizerOne() { + return (configuration) -> configuration.placeholders(Map.of("instance", "schema2-overwrite")); + } + + @Bean + @Order(1) + @FlywayInstance("schema2") + FlywayConfigurationCustomizer customizerTwo() { + return (configuration) -> configuration.connectRetries(5).placeholders(Map.of("instance", "schema2")).baselineDescription("<< Custom baseline schema2 >>"); + } + + @Bean + @Order(0) + FlywayConfigurationCustomizer globalCustomizer() { + return (configuration) -> configuration.connectRetries(10).baselineDescription("<< Global baseline >>"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CallbackConfiguration { + + @Bean + @FlywayInstance("schema2") + Callback schema2CallbackOne() { + return mockCallback("schema2-a"); + } + + @Bean + @FlywayInstance("schema2") + Callback schema2CallbackTwo() { + return mockCallback("schema2-b"); + } + + @Bean + Callback globalCallback() { + return mockCallback("global"); + } + + private Callback mockCallback(String name) { + Callback callback = mock(Callback.class); + given(callback.supports(any(Event.class), any(Context.class))).willReturn(true); + given(callback.getCallbackName()).willReturn(name); + return callback; + } + + } + + @Configuration(proxyBeanMethods = false) + static class FlywayJavaMigrationsConfiguration { + + @Bean + @FlywayInstance("schema2") + TestMigration schema2Migration() { + return new TestMigration("2", "SCHEMA2"); + } + + } + + @Component + @FlywayInstance("schema2") + static class Schema2FlywayMigrationStrategy implements FlywayMigrationStrategy { + + private List defaultSchemas = new ArrayList<>(); + + @Override + public void migrate(Flyway flyway) { + this.defaultSchemas.add(flyway.getConfiguration().getDefaultSchema()); + } + + public List getDefaultSchemas() { + return this.defaultSchemas; + } + } + + } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) .withPropertyValues("spring.datasource.generate-unique-name=true"); @@ -609,13 +856,6 @@ void licenseKeyIsCorrectlyMapped(CapturedOutput output) { + "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) @@ -784,13 +1024,6 @@ void outputQueryResultsIsCorrectlyMapped() { .run(validateFlywayTeamsPropertyOnly("outputQueryResults")); } - @Test - void postgresqlExtensionIsNotLoadedByDefault() { - FluentConfiguration configuration = mock(FluentConfiguration.class); - new PostgresqlFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); - then(configuration).shouldHaveNoInteractions(); - } - @Test void postgresqlTransactionalLockIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -802,13 +1035,6 @@ void postgresqlTransactionalLockIsCorrectlyMapped() { .isTransactionalLock()).isFalse()); } - @Test - void sqlServerExtensionIsNotLoadedByDefault() { - FluentConfiguration configuration = mock(FluentConfiguration.class); - new SqlServerFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); - then(configuration).shouldHaveNoInteractions(); - } - @Test void sqlServerKerberosLoginFileIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -1237,7 +1463,7 @@ private static final class TestMigration implements JavaMigration { private final String description; - private TestMigration(String version, String description) { + TestMigration(String version, String description) { this.version = MigrationVersion.fromVersion(version); this.description = description; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayFactoryTests.java new file mode 100644 index 000000000000..8c46d3d31310 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayFactoryTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.flyway.FlywayFactory.OracleFlywayConfigurationCustomizer; +import org.springframework.boot.autoconfigure.flyway.FlywayFactory.PostgresqlFlywayConfigurationCustomizer; +import org.springframework.boot.autoconfigure.flyway.FlywayFactory.SqlServerFlywayConfigurationCustomizer; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +public class FlywayFactoryTests { + + @Test + void oracleExtensionIsNotLoadedByDefault() { + FluentConfiguration configuration = mock(FluentConfiguration.class); + new OracleFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); + then(configuration).shouldHaveNoInteractions(); + } + + @Test + void sqlServerExtensionIsNotLoadedByDefault() { + FluentConfiguration configuration = mock(FluentConfiguration.class); + new SqlServerFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); + then(configuration).shouldHaveNoInteractions(); + } + + @Test + void postgresqlExtensionIsNotLoadedByDefault() { + FluentConfiguration configuration = mock(FluentConfiguration.class); + new PostgresqlFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); + then(configuration).shouldHaveNoInteractions(); + } + +}