001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.autoconfigure.flyway; 018 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Set; 025import java.util.function.Supplier; 026import java.util.stream.Collectors; 027 028import javax.persistence.EntityManagerFactory; 029import javax.sql.DataSource; 030 031import org.flywaydb.core.Flyway; 032import org.flywaydb.core.api.MigrationVersion; 033import org.flywaydb.core.api.callback.Callback; 034import org.flywaydb.core.api.callback.FlywayCallback; 035import org.flywaydb.core.api.configuration.FluentConfiguration; 036 037import org.springframework.beans.factory.ObjectProvider; 038import org.springframework.boot.autoconfigure.AutoConfigureAfter; 039import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 040import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 041import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 042import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 043import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 044import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; 045import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 046import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 047import org.springframework.boot.autoconfigure.jdbc.JdbcOperationsDependsOnPostProcessor; 048import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; 049import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; 050import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; 051import org.springframework.boot.context.properties.EnableConfigurationProperties; 052import org.springframework.boot.context.properties.PropertyMapper; 053import org.springframework.boot.jdbc.DatabaseDriver; 054import org.springframework.context.annotation.Bean; 055import org.springframework.context.annotation.Configuration; 056import org.springframework.core.convert.TypeDescriptor; 057import org.springframework.core.convert.converter.GenericConverter; 058import org.springframework.core.io.ResourceLoader; 059import org.springframework.jdbc.core.JdbcOperations; 060import org.springframework.jdbc.support.JdbcUtils; 061import org.springframework.jdbc.support.MetaDataAccessException; 062import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; 063import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 064import org.springframework.util.Assert; 065import org.springframework.util.CollectionUtils; 066import org.springframework.util.ObjectUtils; 067import org.springframework.util.StringUtils; 068 069/** 070 * {@link EnableAutoConfiguration Auto-configuration} for Flyway database migrations. 071 * 072 * @author Dave Syer 073 * @author Phillip Webb 074 * @author Vedran Pavic 075 * @author Stephane Nicoll 076 * @author Jacques-Etienne Beaudet 077 * @author EddĂș MelĂ©ndez 078 * @author Dominic Gunn 079 * @since 1.1.0 080 */ 081@SuppressWarnings("deprecation") 082@Configuration 083@ConditionalOnClass(Flyway.class) 084@ConditionalOnBean(DataSource.class) 085@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true) 086@AutoConfigureAfter({ DataSourceAutoConfiguration.class, 087 JdbcTemplateAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) 088public class FlywayAutoConfiguration { 089 090 @Bean 091 @ConfigurationPropertiesBinding 092 public StringOrNumberToMigrationVersionConverter stringOrNumberMigrationVersionConverter() { 093 return new StringOrNumberToMigrationVersionConverter(); 094 } 095 096 @Bean 097 public FlywaySchemaManagementProvider flywayDefaultDdlModeProvider( 098 ObjectProvider<Flyway> flyways) { 099 return new FlywaySchemaManagementProvider(flyways); 100 } 101 102 @Configuration 103 @ConditionalOnMissingBean(Flyway.class) 104 @EnableConfigurationProperties({ DataSourceProperties.class, FlywayProperties.class }) 105 public static class FlywayConfiguration { 106 107 private final FlywayProperties properties; 108 109 private final DataSourceProperties dataSourceProperties; 110 111 private final ResourceLoader resourceLoader; 112 113 private final DataSource dataSource; 114 115 private final DataSource flywayDataSource; 116 117 private final FlywayMigrationStrategy migrationStrategy; 118 119 private final List<FlywayConfigurationCustomizer> configurationCustomizers; 120 121 private final List<Callback> callbacks; 122 123 private final List<FlywayCallback> flywayCallbacks; 124 125 public FlywayConfiguration(FlywayProperties properties, 126 DataSourceProperties dataSourceProperties, ResourceLoader resourceLoader, 127 ObjectProvider<DataSource> dataSource, 128 @FlywayDataSource ObjectProvider<DataSource> flywayDataSource, 129 ObjectProvider<FlywayMigrationStrategy> migrationStrategy, 130 ObjectProvider<FlywayConfigurationCustomizer> fluentConfigurationCustomizers, 131 ObjectProvider<Callback> callbacks, 132 ObjectProvider<FlywayCallback> flywayCallbacks) { 133 this.properties = properties; 134 this.dataSourceProperties = dataSourceProperties; 135 this.resourceLoader = resourceLoader; 136 this.dataSource = dataSource.getIfUnique(); 137 this.flywayDataSource = flywayDataSource.getIfAvailable(); 138 this.migrationStrategy = migrationStrategy.getIfAvailable(); 139 this.configurationCustomizers = fluentConfigurationCustomizers.orderedStream() 140 .collect(Collectors.toList()); 141 this.callbacks = callbacks.orderedStream().collect(Collectors.toList()); 142 this.flywayCallbacks = flywayCallbacks.orderedStream() 143 .collect(Collectors.toList()); 144 } 145 146 @Bean 147 public Flyway flyway() { 148 FluentConfiguration configuration = new FluentConfiguration(); 149 DataSource dataSource = configureDataSource(configuration); 150 checkLocationExists(dataSource); 151 configureProperties(configuration); 152 configureCallbacks(configuration); 153 this.configurationCustomizers 154 .forEach((customizer) -> customizer.customize(configuration)); 155 Flyway flyway = configuration.load(); 156 configureFlywayCallbacks(flyway); 157 return flyway; 158 } 159 160 private DataSource configureDataSource(FluentConfiguration configuration) { 161 if (this.properties.isCreateDataSource()) { 162 String url = getProperty(this.properties::getUrl, 163 this.dataSourceProperties::getUrl); 164 String user = getProperty(this.properties::getUser, 165 this.dataSourceProperties::getUsername); 166 String password = getProperty(this.properties::getPassword, 167 this.dataSourceProperties::getPassword); 168 configuration.dataSource(url, user, password); 169 if (!CollectionUtils.isEmpty(this.properties.getInitSqls())) { 170 String initSql = StringUtils.collectionToDelimitedString( 171 this.properties.getInitSqls(), "\n"); 172 configuration.initSql(initSql); 173 } 174 } 175 else if (this.flywayDataSource != null) { 176 configuration.dataSource(this.flywayDataSource); 177 } 178 else { 179 configuration.dataSource(this.dataSource); 180 } 181 return configuration.getDataSource(); 182 } 183 184 private void checkLocationExists(DataSource dataSource) { 185 if (this.properties.isCheckLocation()) { 186 String[] locations = new LocationResolver(dataSource) 187 .resolveLocations(this.properties.getLocations()); 188 Assert.state(locations.length != 0, 189 "Migration script locations not configured"); 190 boolean exists = hasAtLeastOneLocation(locations); 191 Assert.state(exists, () -> "Cannot find migrations location in: " 192 + Arrays.asList(locations) 193 + " (please add migrations or check your Flyway configuration)"); 194 } 195 } 196 197 private void configureProperties(FluentConfiguration configuration) { 198 PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); 199 String[] locations = new LocationResolver(configuration.getDataSource()) 200 .resolveLocations(this.properties.getLocations()); 201 map.from(locations).to(configuration::locations); 202 map.from(this.properties.getEncoding()).to(configuration::encoding); 203 map.from(this.properties.getConnectRetries()) 204 .to(configuration::connectRetries); 205 map.from(this.properties.getSchemas()).as(StringUtils::toStringArray) 206 .to(configuration::schemas); 207 map.from(this.properties.getTable()).to(configuration::table); 208 map.from(this.properties.getBaselineDescription()) 209 .to(configuration::baselineDescription); 210 map.from(this.properties.getBaselineVersion()) 211 .to(configuration::baselineVersion); 212 map.from(this.properties.getInstalledBy()).to(configuration::installedBy); 213 map.from(this.properties.getPlaceholders()).to(configuration::placeholders); 214 map.from(this.properties.getPlaceholderPrefix()) 215 .to(configuration::placeholderPrefix); 216 map.from(this.properties.getPlaceholderSuffix()) 217 .to(configuration::placeholderSuffix); 218 map.from(this.properties.isPlaceholderReplacement()) 219 .to(configuration::placeholderReplacement); 220 map.from(this.properties.getSqlMigrationPrefix()) 221 .to(configuration::sqlMigrationPrefix); 222 map.from(this.properties.getSqlMigrationSuffixes()) 223 .as(StringUtils::toStringArray) 224 .to(configuration::sqlMigrationSuffixes); 225 map.from(this.properties.getSqlMigrationSeparator()) 226 .to(configuration::sqlMigrationSeparator); 227 map.from(this.properties.getRepeatableSqlMigrationPrefix()) 228 .to(configuration::repeatableSqlMigrationPrefix); 229 map.from(this.properties.getTarget()).to(configuration::target); 230 map.from(this.properties.isBaselineOnMigrate()) 231 .to(configuration::baselineOnMigrate); 232 map.from(this.properties.isCleanDisabled()).to(configuration::cleanDisabled); 233 map.from(this.properties.isCleanOnValidationError()) 234 .to(configuration::cleanOnValidationError); 235 map.from(this.properties.isGroup()).to(configuration::group); 236 map.from(this.properties.isIgnoreMissingMigrations()) 237 .to(configuration::ignoreMissingMigrations); 238 map.from(this.properties.isIgnoreIgnoredMigrations()) 239 .to(configuration::ignoreIgnoredMigrations); 240 map.from(this.properties.isIgnorePendingMigrations()) 241 .to(configuration::ignorePendingMigrations); 242 map.from(this.properties.isIgnoreFutureMigrations()) 243 .to(configuration::ignoreFutureMigrations); 244 map.from(this.properties.isMixed()).to(configuration::mixed); 245 map.from(this.properties.isOutOfOrder()).to(configuration::outOfOrder); 246 map.from(this.properties.isSkipDefaultCallbacks()) 247 .to(configuration::skipDefaultCallbacks); 248 map.from(this.properties.isSkipDefaultResolvers()) 249 .to(configuration::skipDefaultResolvers); 250 map.from(this.properties.isValidateOnMigrate()) 251 .to(configuration::validateOnMigrate); 252 } 253 254 private void configureCallbacks(FluentConfiguration configuration) { 255 if (!this.callbacks.isEmpty()) { 256 configuration.callbacks(this.callbacks.toArray(new Callback[0])); 257 } 258 } 259 260 private void configureFlywayCallbacks(Flyway flyway) { 261 if (!this.flywayCallbacks.isEmpty()) { 262 if (!this.callbacks.isEmpty()) { 263 throw new IllegalStateException( 264 "Found a mixture of Callback and FlywayCallback beans." 265 + " One type must be used exclusively."); 266 } 267 flyway.setCallbacks(this.flywayCallbacks.toArray(new FlywayCallback[0])); 268 } 269 } 270 271 private String getProperty(Supplier<String> property, 272 Supplier<String> defaultValue) { 273 String value = property.get(); 274 return (value != null) ? value : defaultValue.get(); 275 } 276 277 private boolean hasAtLeastOneLocation(String... locations) { 278 for (String location : locations) { 279 if (this.resourceLoader.getResource(normalizePrefix(location)).exists()) { 280 return true; 281 } 282 } 283 return false; 284 } 285 286 private String normalizePrefix(String location) { 287 return location.replace("filesystem:", "file:"); 288 } 289 290 @Bean 291 @ConditionalOnMissingBean 292 public FlywayMigrationInitializer flywayInitializer(Flyway flyway) { 293 return new FlywayMigrationInitializer(flyway, this.migrationStrategy); 294 } 295 296 /** 297 * Additional configuration to ensure that {@link EntityManagerFactory} beans 298 * depend on the {@code flywayInitializer} bean. 299 */ 300 @Configuration 301 @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class) 302 @ConditionalOnBean(AbstractEntityManagerFactoryBean.class) 303 protected static class FlywayInitializerJpaDependencyConfiguration 304 extends EntityManagerFactoryDependsOnPostProcessor { 305 306 public FlywayInitializerJpaDependencyConfiguration() { 307 super("flywayInitializer"); 308 } 309 310 } 311 312 /** 313 * Additional configuration to ensure that {@link JdbcOperations} beans depend on 314 * the {@code flywayInitializer} bean. 315 */ 316 @Configuration 317 @ConditionalOnClass(JdbcOperations.class) 318 @ConditionalOnBean(JdbcOperations.class) 319 protected static class FlywayInitializerJdbcOperationsDependencyConfiguration 320 extends JdbcOperationsDependsOnPostProcessor { 321 322 public FlywayInitializerJdbcOperationsDependencyConfiguration() { 323 super("flywayInitializer"); 324 } 325 326 } 327 328 } 329 330 /** 331 * Additional configuration to ensure that {@link EntityManagerFactory} beans depend 332 * on the {@code flyway} bean. 333 */ 334 @Configuration 335 @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class) 336 @ConditionalOnBean(AbstractEntityManagerFactoryBean.class) 337 protected static class FlywayJpaDependencyConfiguration 338 extends EntityManagerFactoryDependsOnPostProcessor { 339 340 public FlywayJpaDependencyConfiguration() { 341 super("flyway"); 342 } 343 344 } 345 346 /** 347 * Additional configuration to ensure that {@link JdbcOperations} beans depend on the 348 * {@code flyway} bean. 349 */ 350 @Configuration 351 @ConditionalOnClass(JdbcOperations.class) 352 @ConditionalOnBean(JdbcOperations.class) 353 protected static class FlywayJdbcOperationsDependencyConfiguration 354 extends JdbcOperationsDependsOnPostProcessor { 355 356 public FlywayJdbcOperationsDependencyConfiguration() { 357 super("flyway"); 358 } 359 360 } 361 362 private static class LocationResolver { 363 364 private static final String VENDOR_PLACEHOLDER = "{vendor}"; 365 366 private final DataSource dataSource; 367 368 LocationResolver(DataSource dataSource) { 369 this.dataSource = dataSource; 370 } 371 372 public String[] resolveLocations(Collection<String> locations) { 373 return resolveLocations(StringUtils.toStringArray(locations)); 374 } 375 376 public String[] resolveLocations(String[] locations) { 377 if (usesVendorLocation(locations)) { 378 DatabaseDriver databaseDriver = getDatabaseDriver(); 379 return replaceVendorLocations(locations, databaseDriver); 380 } 381 return locations; 382 } 383 384 private String[] replaceVendorLocations(String[] locations, 385 DatabaseDriver databaseDriver) { 386 if (databaseDriver == DatabaseDriver.UNKNOWN) { 387 return locations; 388 } 389 String vendor = databaseDriver.getId(); 390 return Arrays.stream(locations) 391 .map((location) -> location.replace(VENDOR_PLACEHOLDER, vendor)) 392 .toArray(String[]::new); 393 } 394 395 private DatabaseDriver getDatabaseDriver() { 396 try { 397 String url = JdbcUtils.extractDatabaseMetaData(this.dataSource, "getURL"); 398 return DatabaseDriver.fromJdbcUrl(url); 399 } 400 catch (MetaDataAccessException ex) { 401 throw new IllegalStateException(ex); 402 } 403 404 } 405 406 private boolean usesVendorLocation(String... locations) { 407 for (String location : locations) { 408 if (location.contains(VENDOR_PLACEHOLDER)) { 409 return true; 410 } 411 } 412 return false; 413 } 414 415 } 416 417 /** 418 * Convert a String or Number to a {@link MigrationVersion}. 419 */ 420 private static class StringOrNumberToMigrationVersionConverter 421 implements GenericConverter { 422 423 private static final Set<ConvertiblePair> CONVERTIBLE_TYPES; 424 425 static { 426 Set<ConvertiblePair> types = new HashSet<>(2); 427 types.add(new ConvertiblePair(String.class, MigrationVersion.class)); 428 types.add(new ConvertiblePair(Number.class, MigrationVersion.class)); 429 CONVERTIBLE_TYPES = Collections.unmodifiableSet(types); 430 } 431 432 @Override 433 public Set<ConvertiblePair> getConvertibleTypes() { 434 return CONVERTIBLE_TYPES; 435 } 436 437 @Override 438 public Object convert(Object source, TypeDescriptor sourceType, 439 TypeDescriptor targetType) { 440 String value = ObjectUtils.nullSafeToString(source); 441 return MigrationVersion.fromVersion(value); 442 } 443 444 } 445 446}