001/* 002 * Copyright 2012-2016 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.Collections; 020import java.util.HashSet; 021import java.util.Set; 022 023import javax.annotation.PostConstruct; 024import javax.persistence.EntityManagerFactory; 025import javax.sql.DataSource; 026 027import org.flywaydb.core.Flyway; 028import org.flywaydb.core.api.MigrationVersion; 029 030import org.springframework.beans.factory.ObjectProvider; 031import org.springframework.boot.autoconfigure.AutoConfigureAfter; 032import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 033import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 034import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 035import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 036import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 037import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; 038import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 039import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; 040import org.springframework.boot.context.properties.ConfigurationProperties; 041import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; 042import org.springframework.boot.context.properties.EnableConfigurationProperties; 043import org.springframework.boot.jdbc.DatabaseDriver; 044import org.springframework.context.annotation.Bean; 045import org.springframework.context.annotation.Configuration; 046import org.springframework.core.convert.TypeDescriptor; 047import org.springframework.core.convert.converter.GenericConverter; 048import org.springframework.core.io.ResourceLoader; 049import org.springframework.jdbc.support.JdbcUtils; 050import org.springframework.jdbc.support.MetaDataAccessException; 051import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; 052import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 053import org.springframework.util.Assert; 054import org.springframework.util.ObjectUtils; 055 056/** 057 * {@link EnableAutoConfiguration Auto-configuration} for Flyway database migrations. 058 * 059 * @author Dave Syer 060 * @author Phillip Webb 061 * @author Vedran Pavic 062 * @author Stephane Nicoll 063 * @author Jacques-Etienne Beaudet 064 * @since 1.1.0 065 */ 066@Configuration 067@ConditionalOnClass(Flyway.class) 068@ConditionalOnBean(DataSource.class) 069@ConditionalOnProperty(prefix = "flyway", name = "enabled", matchIfMissing = true) 070@AutoConfigureAfter({ DataSourceAutoConfiguration.class, 071 HibernateJpaAutoConfiguration.class }) 072public class FlywayAutoConfiguration { 073 074 @Bean 075 @ConfigurationPropertiesBinding 076 public StringOrNumberToMigrationVersionConverter stringOrNumberMigrationVersionConverter() { 077 return new StringOrNumberToMigrationVersionConverter(); 078 } 079 080 @Configuration 081 @ConditionalOnMissingBean(Flyway.class) 082 @EnableConfigurationProperties(FlywayProperties.class) 083 public static class FlywayConfiguration { 084 085 private final FlywayProperties properties; 086 087 private final ResourceLoader resourceLoader; 088 089 private final DataSource dataSource; 090 091 private final DataSource flywayDataSource; 092 093 private final FlywayMigrationStrategy migrationStrategy; 094 095 public FlywayConfiguration(FlywayProperties properties, 096 ResourceLoader resourceLoader, ObjectProvider<DataSource> dataSource, 097 @FlywayDataSource ObjectProvider<DataSource> flywayDataSource, 098 ObjectProvider<FlywayMigrationStrategy> migrationStrategy) { 099 this.properties = properties; 100 this.resourceLoader = resourceLoader; 101 this.dataSource = dataSource.getIfUnique(); 102 this.flywayDataSource = flywayDataSource.getIfAvailable(); 103 this.migrationStrategy = migrationStrategy.getIfAvailable(); 104 } 105 106 @PostConstruct 107 public void checkLocationExists() { 108 if (this.properties.isCheckLocation()) { 109 Assert.state(!this.properties.getLocations().isEmpty(), 110 "Migration script locations not configured"); 111 boolean exists = hasAtLeastOneLocation(); 112 Assert.state(exists, 113 "Cannot find migrations location in: " + this.properties 114 .getLocations() 115 + " (please add migrations or check your Flyway configuration)"); 116 } 117 } 118 119 private boolean hasAtLeastOneLocation() { 120 for (String location : this.properties.getLocations()) { 121 if (this.resourceLoader.getResource(location).exists()) { 122 return true; 123 } 124 } 125 return false; 126 } 127 128 @Bean 129 @ConfigurationProperties(prefix = "flyway") 130 public Flyway flyway() { 131 Flyway flyway = new SpringBootFlyway(); 132 if (this.properties.isCreateDataSource()) { 133 flyway.setDataSource(this.properties.getUrl(), this.properties.getUser(), 134 this.properties.getPassword(), 135 this.properties.getInitSqls().toArray(new String[0])); 136 } 137 else if (this.flywayDataSource != null) { 138 flyway.setDataSource(this.flywayDataSource); 139 } 140 else { 141 flyway.setDataSource(this.dataSource); 142 } 143 flyway.setLocations(this.properties.getLocations().toArray(new String[0])); 144 return flyway; 145 } 146 147 @Bean 148 @ConditionalOnMissingBean 149 public FlywayMigrationInitializer flywayInitializer(Flyway flyway) { 150 return new FlywayMigrationInitializer(flyway, this.migrationStrategy); 151 } 152 153 /** 154 * Additional configuration to ensure that {@link EntityManagerFactory} beans 155 * depend-on the {@code flywayInitializer} bean. 156 */ 157 @Configuration 158 @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class) 159 @ConditionalOnBean(AbstractEntityManagerFactoryBean.class) 160 protected static class FlywayInitializerJpaDependencyConfiguration 161 extends EntityManagerFactoryDependsOnPostProcessor { 162 163 public FlywayInitializerJpaDependencyConfiguration() { 164 super("flywayInitializer"); 165 } 166 167 } 168 169 } 170 171 /** 172 * Additional configuration to ensure that {@link EntityManagerFactory} beans 173 * depend-on the {@code flyway} bean. 174 */ 175 @Configuration 176 @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class) 177 @ConditionalOnBean(AbstractEntityManagerFactoryBean.class) 178 protected static class FlywayJpaDependencyConfiguration 179 extends EntityManagerFactoryDependsOnPostProcessor { 180 181 public FlywayJpaDependencyConfiguration() { 182 super("flyway"); 183 } 184 185 } 186 187 private static class SpringBootFlyway extends Flyway { 188 189 private static final String VENDOR_PLACEHOLDER = "{vendor}"; 190 191 @Override 192 public void setLocations(String... locations) { 193 if (usesVendorLocation(locations)) { 194 try { 195 String url = (String) JdbcUtils 196 .extractDatabaseMetaData(getDataSource(), "getURL"); 197 DatabaseDriver vendor = DatabaseDriver.fromJdbcUrl(url); 198 if (vendor != DatabaseDriver.UNKNOWN) { 199 for (int i = 0; i < locations.length; i++) { 200 locations[i] = locations[i].replace(VENDOR_PLACEHOLDER, 201 vendor.getId()); 202 } 203 } 204 } 205 catch (MetaDataAccessException ex) { 206 throw new IllegalStateException(ex); 207 } 208 } 209 super.setLocations(locations); 210 } 211 212 private boolean usesVendorLocation(String... locations) { 213 for (String location : locations) { 214 if (location.contains(VENDOR_PLACEHOLDER)) { 215 return true; 216 } 217 } 218 return false; 219 } 220 221 } 222 223 /** 224 * Convert a String or Number to a {@link MigrationVersion}. 225 */ 226 private static class StringOrNumberToMigrationVersionConverter 227 implements GenericConverter { 228 229 private static final Set<ConvertiblePair> CONVERTIBLE_TYPES; 230 231 static { 232 Set<ConvertiblePair> types = new HashSet<ConvertiblePair>(2); 233 types.add(new ConvertiblePair(String.class, MigrationVersion.class)); 234 types.add(new ConvertiblePair(Number.class, MigrationVersion.class)); 235 CONVERTIBLE_TYPES = Collections.unmodifiableSet(types); 236 } 237 238 @Override 239 public Set<ConvertiblePair> getConvertibleTypes() { 240 return CONVERTIBLE_TYPES; 241 } 242 243 @Override 244 public Object convert(Object source, TypeDescriptor sourceType, 245 TypeDescriptor targetType) { 246 String value = ObjectUtils.nullSafeToString(source); 247 return MigrationVersion.fromVersion(value); 248 } 249 250 } 251 252}