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}