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}