Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@
import liquibase.exception.LiquibaseException;
import liquibase.integration.spring.SpringLiquibase;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.util.ReflectionUtils;

/**
* A custom {@link SpringLiquibase} extension that closes the underlying
* {@link DataSource} once the database has been migrated.
*
* @author Andy Wilkinson
* @author Dylan Miska
* @since 4.0.0
*/
public class DataSourceClosingSpringLiquibase extends SpringLiquibase implements DisposableBean {
public class DataSourceClosingSpringLiquibase extends EnvironmentAwareSpringLiquibase {

private volatile boolean closeDataSourceOnceMigrated = true;

Expand All @@ -58,7 +58,8 @@ private void closeDataSource() {
}

@Override
public void destroy() throws Exception {
public void destroy() {
super.destroy();
if (!this.closeDataSourceOnceMigrated) {
closeDataSource();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.liquibase.autoconfigure;

import liquibase.Scope;
import liquibase.exception.LiquibaseException;
import liquibase.integration.spring.SpringLiquibase;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.util.HashMap;
import java.util.Map;

/**
* A {@link SpringLiquibase} subclass that creates a Liquibase child scope with a
* reference to the Spring Environment, allowing the
* {@link EnvironmentConfigurationValueProvider} to access the correct Environment for
* configuration values.
*
* @author Dylan Miska
*/
class EnvironmentAwareSpringLiquibase extends SpringLiquibase implements ApplicationContextAware, DisposableBean {

private @Nullable String environmentId;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.environmentId = applicationContext.getId();
}

@Override
public void afterPropertiesSet() throws LiquibaseException {
try {
Map<String, Object> scopeValues = new HashMap<>();
scopeValues.put(EnvironmentConfigurationValueProvider.SPRING_ENV_ID_KEY, this.environmentId);
Scope.child(scopeValues, EnvironmentAwareSpringLiquibase.super::afterPropertiesSet);
}
catch (Exception ex) {
if (ex instanceof LiquibaseException) {
throw (LiquibaseException) ex;
}
throw new LiquibaseException(ex);
}
}

@Override
public void destroy() {
if (this.environmentId != null) {
EnvironmentConfigurationValueProvider.unregisterEnvironment(this.environmentId);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.liquibase.autoconfigure;

import liquibase.configuration.AbstractConfigurationValueProvider;
import liquibase.configuration.ProvidedValue;
import org.jspecify.annotations.Nullable;
import org.springframework.core.env.Environment;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* A Liquibase {@code ConfigurationValueProvider} that passes through properties defined
* in the Spring {@link Environment} using the exact-name convention:
*
* <pre>
* spring.liquibase.properties.&lt;liquibaseKey&gt; = &lt;value&gt;
* </pre>
*
* For example: <pre>
* spring.liquibase.properties.liquibase.duplicateFileMode = WARN
* spring.liquibase.properties.liquibase.searchPath = classpath:/db,file:./external
* </pre>
*
* No relaxed binding or key transformation is performed. Keys are looked up exactly as
* provided by Liquibase (including dots and casing), prefixed with
* {@code spring.liquibase.properties.}.
*
* @author Dylan Miska
*/
public final class EnvironmentConfigurationValueProvider extends AbstractConfigurationValueProvider {

private static final String PREFIX = "spring.liquibase.properties.";

public static final String SPRING_ENV_ID_KEY = "spring.environment.id";

private static final Map<String, Environment> environments = new ConcurrentHashMap<>();

static void registerEnvironment(String environmentId, Environment environment) {
environments.put(environmentId, environment);
}

static void unregisterEnvironment(String environmentId) {
environments.remove(environmentId);
}

@Override
public int getPrecedence() {
return 100;
}

@Override
public @Nullable ProvidedValue getProvidedValue(String... keyAndAliases) {
if (keyAndAliases == null) {
return null;
}

String environmentId = liquibase.Scope.getCurrentScope().get(SPRING_ENV_ID_KEY, String.class);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add additional strategies here, such as inferring the env if there's only one, for example. I opted for the simplest solution for now, but would love input on this.

if (environmentId == null) {
return null;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered adding logging here if we get a request for a config from this provider but we can't infer the env but I didn't see any other logging in the module, so I skipped for now.

}

Environment environment = environments.get(environmentId);
if (environment == null) {
return null;
}

for (String requestedKey : keyAndAliases) {
if (requestedKey != null) {
String propertyName = PREFIX + requestedKey;
String value = environment.getProperty(propertyName);
if (value != null) {
return new ProvidedValue(requestedKey, requestedKey, value,
"Spring Environment property '" + propertyName + "'", this);
}
}
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.boot.liquibase.autoconfigure;

import java.util.Objects;

import javax.sql.DataSource;

import liquibase.Liquibase;
Expand Down Expand Up @@ -45,6 +47,7 @@
import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration.LiquibaseAutoConfigurationRuntimeHints;
import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration.LiquibaseDataSourceCondition;
import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
Expand All @@ -71,6 +74,7 @@
* @author Evgeniy Cheban
* @author Moritz Halbritter
* @author Ahmed Ashour
* @author Dylan Miska
* @since 4.0.0
*/
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
Expand Down Expand Up @@ -101,7 +105,9 @@ PropertiesLiquibaseConnectionDetails liquibaseConnectionDetails(LiquibasePropert
@Bean
SpringLiquibase liquibase(ObjectProvider<DataSource> dataSource,
@LiquibaseDataSource ObjectProvider<DataSource> liquibaseDataSource, LiquibaseProperties properties,
ObjectProvider<SpringLiquibaseCustomizer> customizers, LiquibaseConnectionDetails connectionDetails) {
ObjectProvider<SpringLiquibaseCustomizer> customizers, LiquibaseConnectionDetails connectionDetails,
ApplicationContext applicationContext) {
registerLiquibaseConfigurationValueProvider(applicationContext);
SpringLiquibase liquibase = createSpringLiquibase(liquibaseDataSource.getIfAvailable(),
dataSource.getIfUnique(), connectionDetails);
liquibase.setChangeLog(properties.getChangeLog());
Expand Down Expand Up @@ -147,12 +153,28 @@ private SpringLiquibase createSpringLiquibase(@Nullable DataSource liquibaseData
@Nullable DataSource dataSource, LiquibaseConnectionDetails connectionDetails) {
DataSource migrationDataSource = getMigrationDataSource(liquibaseDataSource, dataSource, connectionDetails);
SpringLiquibase liquibase = (migrationDataSource == liquibaseDataSource
|| migrationDataSource == dataSource) ? new SpringLiquibase()
|| migrationDataSource == dataSource) ? new EnvironmentAwareSpringLiquibase()
: new DataSourceClosingSpringLiquibase();
liquibase.setDataSource(migrationDataSource);
return liquibase;
}

private void registerLiquibaseConfigurationValueProvider(ApplicationContext applicationContext) {
liquibase.configuration.LiquibaseConfiguration liquibaseConfiguration = liquibase.Scope.getCurrentScope()
.getSingleton(liquibase.configuration.LiquibaseConfiguration.class);

boolean providerExists = liquibaseConfiguration.getProviders()
.stream()
.anyMatch((provider) -> provider.getClass() == EnvironmentConfigurationValueProvider.class);

if (!providerExists) {
liquibaseConfiguration.registerProvider(new EnvironmentConfigurationValueProvider());
}

EnvironmentConfigurationValueProvider.registerEnvironment(
Objects.requireNonNull(applicationContext.getId()), applicationContext.getEnvironment());
}

private DataSource getMigrationDataSource(@Nullable DataSource liquibaseDataSource,
@Nullable DataSource dataSource, LiquibaseConnectionDetails connectionDetails) {
if (liquibaseDataSource != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
* @author Eddú Meléndez
* @author Ferenc Gratzer
* @author Evgeniy Cheban
* @author Dylan Miska
* @since 4.0.0
*/
@ConfigurationProperties(prefix = "spring.liquibase", ignoreUnknownFields = false)
Expand Down Expand Up @@ -123,6 +124,15 @@ public class LiquibaseProperties {
*/
private @Nullable Map<String, String> parameters;

/**
* Liquibase global configuration properties. Properties must be set with liquibase's
* full propertiesFile dot format. For example:
* {@code spring.liquibase.properties.liquibase.duplicateFileMode}. Note that
* Liquibase’s normal precedence still applies (env variables and jvm system
* properties can override values set here).
*/
private @Nullable Map<String, String> properties;

/**
* File to which rollback SQL is written when an update is performed.
*/
Expand Down Expand Up @@ -294,6 +304,14 @@ public void setParameters(@Nullable Map<String, String> parameters) {
this.parameters = parameters;
}

public @Nullable Map<String, String> getProperties() {
return this.properties;
}

public void setProperties(@Nullable Map<String, String> properties) {
this.properties = properties;
}

public @Nullable File getRollbackFile() {
return this.rollbackFile;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.sql.DataSource;

import liquibase.Scope;
import liquibase.changelog.ChangeSet.ExecType;
import liquibase.changelog.RanChangeSet;
import liquibase.changelog.StandardChangeLogHistoryService;
Expand All @@ -36,6 +38,7 @@
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.liquibase.autoconfigure.EnvironmentConfigurationValueProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
Expand All @@ -45,6 +48,7 @@
*
* @author Eddú Meléndez
* @author Nabil Fawwaz Elqayyim
* @author Dylan Miska
* @since 4.0.0
*/
@Endpoint(id = "liquibase")
Expand All @@ -58,18 +62,26 @@ public LiquibaseEndpoint(ApplicationContext context) {
}

@ReadOperation
public LiquibaseBeansDescriptor liquibaseBeans() {
public LiquibaseBeansDescriptor liquibaseBeans() throws Exception {
ApplicationContext target = this.context;
Map<@Nullable String, ContextLiquibaseBeansDescriptor> contextBeans = new HashMap<>();
while (target != null) {
Map<String, LiquibaseBeanDescriptor> liquibaseBeans = new HashMap<>();
DatabaseFactory factory = DatabaseFactory.getInstance();
target.getBeansOfType(SpringLiquibase.class)
.forEach((name, liquibase) -> liquibaseBeans.put(name, createReport(liquibase, factory)));
ApplicationContext parent = target.getParent();
contextBeans.put(target.getId(),
new ContextLiquibaseBeansDescriptor(liquibaseBeans, (parent != null) ? parent.getId() : null));
target = parent;
Map<String, Object> scopeValues = new HashMap<>();
scopeValues.put(EnvironmentConfigurationValueProvider.SPRING_ENV_ID_KEY,
Objects.requireNonNull(target.getId()));
ApplicationContext currentContext = target;
Scope.child(scopeValues, () -> {
Map<String, LiquibaseBeanDescriptor> liquibaseBeans = new HashMap<>();
DatabaseFactory factory = DatabaseFactory.getInstance();
currentContext.getBeansOfType(SpringLiquibase.class)
.forEach((name, liquibase) -> liquibaseBeans.put(name, createReport(liquibase, factory)));
ApplicationContext parent = currentContext.getParent();
contextBeans.put(currentContext.getId(),
new ContextLiquibaseBeansDescriptor(liquibaseBeans, (parent != null) ? parent.getId() : null));
return null;
});

target = target.getParent();
}
return new LiquibaseBeansDescriptor(contextBeans);
}
Expand Down
Loading