001/*
002 * Copyright 2012-2017 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.liquibase;
018
019import java.lang.reflect.Method;
020
021import javax.annotation.PostConstruct;
022import javax.persistence.EntityManagerFactory;
023import javax.sql.DataSource;
024
025import liquibase.exception.LiquibaseException;
026import liquibase.integration.spring.SpringLiquibase;
027
028import org.springframework.beans.factory.ObjectProvider;
029import org.springframework.boot.autoconfigure.AutoConfigureAfter;
030import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
031import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
032import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
033import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
034import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
035import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor;
036import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
037import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
038import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
039import org.springframework.boot.context.properties.EnableConfigurationProperties;
040import org.springframework.context.annotation.Bean;
041import org.springframework.context.annotation.Configuration;
042import org.springframework.context.annotation.Import;
043import org.springframework.core.io.Resource;
044import org.springframework.core.io.ResourceLoader;
045import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
046import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
047import org.springframework.util.Assert;
048import org.springframework.util.ReflectionUtils;
049
050/**
051 * {@link EnableAutoConfiguration Auto-configuration} for Liquibase.
052 *
053 * @author Marcel Overdijk
054 * @author Dave Syer
055 * @author Phillip Webb
056 * @author Eddú Meléndez
057 * @author Andy Wilkinson
058 * @since 1.1.0
059 */
060@Configuration
061@ConditionalOnClass(SpringLiquibase.class)
062@ConditionalOnBean(DataSource.class)
063@ConditionalOnProperty(prefix = "liquibase", name = "enabled", matchIfMissing = true)
064@AutoConfigureAfter({ DataSourceAutoConfiguration.class,
065                HibernateJpaAutoConfiguration.class })
066public class LiquibaseAutoConfiguration {
067
068        @Configuration
069        @ConditionalOnMissingBean(SpringLiquibase.class)
070        @EnableConfigurationProperties(LiquibaseProperties.class)
071        @Import(LiquibaseJpaDependencyConfiguration.class)
072        public static class LiquibaseConfiguration {
073
074                private final LiquibaseProperties properties;
075
076                private final ResourceLoader resourceLoader;
077
078                private final DataSource dataSource;
079
080                private final DataSource liquibaseDataSource;
081
082                public LiquibaseConfiguration(LiquibaseProperties properties,
083                                ResourceLoader resourceLoader, ObjectProvider<DataSource> dataSource,
084                                @LiquibaseDataSource ObjectProvider<DataSource> liquibaseDataSource) {
085                        this.properties = properties;
086                        this.resourceLoader = resourceLoader;
087                        this.dataSource = dataSource.getIfUnique();
088                        this.liquibaseDataSource = liquibaseDataSource.getIfAvailable();
089                }
090
091                @PostConstruct
092                public void checkChangelogExists() {
093                        if (this.properties.isCheckChangeLogLocation()) {
094                                Resource resource = this.resourceLoader
095                                                .getResource(this.properties.getChangeLog());
096                                Assert.state(resource.exists(),
097                                                "Cannot find changelog location: " + resource
098                                                                + " (please add changelog or check your Liquibase "
099                                                                + "configuration)");
100                        }
101                }
102
103                @Bean
104                public SpringLiquibase liquibase() {
105                        SpringLiquibase liquibase = createSpringLiquibase();
106                        liquibase.setChangeLog(this.properties.getChangeLog());
107                        liquibase.setContexts(this.properties.getContexts());
108                        liquibase.setDefaultSchema(this.properties.getDefaultSchema());
109                        liquibase.setDropFirst(this.properties.isDropFirst());
110                        liquibase.setShouldRun(this.properties.isEnabled());
111                        liquibase.setLabels(this.properties.getLabels());
112                        liquibase.setChangeLogParameters(this.properties.getParameters());
113                        liquibase.setRollbackFile(this.properties.getRollbackFile());
114                        return liquibase;
115                }
116
117                private SpringLiquibase createSpringLiquibase() {
118                        DataSource liquibaseDataSource = getDataSource();
119                        if (liquibaseDataSource != null) {
120                                SpringLiquibase liquibase = new SpringLiquibase();
121                                liquibase.setDataSource(liquibaseDataSource);
122                                return liquibase;
123                        }
124                        SpringLiquibase liquibase = new DataSourceClosingSpringLiquibase();
125                        liquibase.setDataSource(createNewDataSource());
126                        return liquibase;
127                }
128
129                private DataSource getDataSource() {
130                        if (this.liquibaseDataSource != null) {
131                                return this.liquibaseDataSource;
132                        }
133                        if (this.properties.getUrl() == null) {
134                                return this.dataSource;
135                        }
136                        return null;
137                }
138
139                private DataSource createNewDataSource() {
140                        return DataSourceBuilder.create().url(this.properties.getUrl())
141                                        .username(this.properties.getUser())
142                                        .password(this.properties.getPassword()).build();
143                }
144
145        }
146
147        /**
148         * Additional configuration to ensure that {@link EntityManagerFactory} beans
149         * depend-on the liquibase bean.
150         */
151        @Configuration
152        @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
153        @ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
154        protected static class LiquibaseJpaDependencyConfiguration
155                        extends EntityManagerFactoryDependsOnPostProcessor {
156
157                public LiquibaseJpaDependencyConfiguration() {
158                        super("liquibase");
159                }
160
161        }
162
163        /**
164         * A custom {@link SpringLiquibase} extension that closes the underlying
165         * {@link DataSource} once the database has been migrated.
166         */
167        private static final class DataSourceClosingSpringLiquibase extends SpringLiquibase {
168
169                @Override
170                public void afterPropertiesSet() throws LiquibaseException {
171                        super.afterPropertiesSet();
172                        closeDataSource();
173                }
174
175                private void closeDataSource() {
176                        Class<?> dataSourceClass = getDataSource().getClass();
177                        Method closeMethod = ReflectionUtils.findMethod(dataSourceClass, "close");
178                        if (closeMethod != null) {
179                                ReflectionUtils.invokeMethod(closeMethod, getDataSource());
180                        }
181                }
182
183        }
184
185}