Skip to content

Commit 19761a2

Browse files
committed
Add EnvironmentConfigurationValueProvider for Liquibase property pass-through
Signed-off-by: Dylan Miska <[email protected]>
1 parent 862db41 commit 19761a2

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.liquibase.autoconfigure;
18+
19+
import liquibase.configuration.AbstractConfigurationValueProvider;
20+
import liquibase.configuration.ProvidedValue;
21+
import org.jspecify.annotations.Nullable;
22+
import org.springframework.core.env.Environment;
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
* A Liquibase {@code ConfigurationValueProvider} that passes through properties defined
27+
* in the Spring {@link Environment} using the exact-name convention:
28+
*
29+
* <pre>
30+
* spring.liquibase.properties.&lt;liquibaseKey&gt; = &lt;value&gt;
31+
* </pre>
32+
*
33+
* For example: <pre>
34+
* spring.liquibase.properties.liquibase.duplicateFileMode = WARN
35+
* spring.liquibase.properties.liquibase.searchPath = classpath:/db,file:./external
36+
* </pre>
37+
*
38+
* No relaxed binding or key transformation is performed. Keys are looked up exactly as
39+
* provided by Liquibase (including dots and casing), prefixed with
40+
* {@code spring.liquibase.properties.}.
41+
*/
42+
final class EnvironmentConfigurationValueProvider extends AbstractConfigurationValueProvider {
43+
44+
private static final String PREFIX = "spring.liquibase.properties.";
45+
46+
private final Environment environment;
47+
48+
EnvironmentConfigurationValueProvider(Environment environment) {
49+
Assert.notNull(environment, "Environment must not be null");
50+
this.environment = environment;
51+
}
52+
53+
@Override
54+
public int getPrecedence() {
55+
return 100;
56+
}
57+
58+
@Override
59+
public @Nullable ProvidedValue getProvidedValue(String... keyAndAliases) {
60+
if (keyAndAliases == null) {
61+
return null;
62+
}
63+
for (String requestedKey : keyAndAliases) {
64+
if (requestedKey == null) {
65+
continue;
66+
}
67+
// Exact pass-through: no rewriting of the Liquibase key
68+
String propertyName = PREFIX + requestedKey;
69+
String value = this.environment.getProperty(propertyName);
70+
if (value != null) {
71+
// requestedKey: the key Liquibase asked for
72+
// propertyName: the exact Spring Environment property we read
73+
return new ProvidedValue(requestedKey, requestedKey, value,
74+
"Spring Environment property '" + propertyName + "'", this);
75+
}
76+
}
77+
return null;
78+
}
79+
80+
}

module/spring-boot-liquibase/src/main/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfiguration.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.aot.hint.RuntimeHints;
3131
import org.springframework.aot.hint.RuntimeHintsRegistrar;
3232
import org.springframework.beans.factory.ObjectProvider;
33+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
3334
import org.springframework.boot.autoconfigure.AutoConfiguration;
3435
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3536
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
@@ -50,6 +51,7 @@
5051
import org.springframework.context.annotation.Configuration;
5152
import org.springframework.context.annotation.Import;
5253
import org.springframework.context.annotation.ImportRuntimeHints;
54+
import org.springframework.core.env.Environment;
5355
import org.springframework.jdbc.core.ConnectionCallback;
5456
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
5557
import org.springframework.util.Assert;
@@ -188,6 +190,24 @@ private void applyConnectionDetails(LiquibaseConnectionDetails connectionDetails
188190

189191
}
190192

193+
@Bean
194+
static BeanFactoryPostProcessor liquibaseConfigurationValueProviderRegistrar(Environment environment) {
195+
196+
return (beanFactory) -> {
197+
var liquibaseConfiguration = liquibase.Scope.getCurrentScope()
198+
.getSingleton(liquibase.configuration.LiquibaseConfiguration.class);
199+
200+
// Remove any previously registered instance of our provider class
201+
liquibaseConfiguration.getProviders()
202+
.stream()
203+
.filter((provider) -> provider.getClass() == EnvironmentConfigurationValueProvider.class)
204+
.toList()
205+
.forEach(liquibaseConfiguration::unregisterProvider);
206+
207+
liquibaseConfiguration.registerProvider(new EnvironmentConfigurationValueProvider(environment));
208+
};
209+
}
210+
191211
@ConditionalOnClass(Customizer.class)
192212
@Configuration(proxyBeanMethods = false)
193213
static class CustomizerConfiguration {

module/spring-boot-liquibase/src/main/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseProperties.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ public class LiquibaseProperties {
123123
*/
124124
private @Nullable Map<String, String> parameters;
125125

126+
/**
127+
* Liquibase global configuration properties. Properties must be set with liquibase's
128+
* full propertiesFile dot format. For example:
129+
* {@code spring.liquibase.properties.liquibase.duplicateFileMode}. Note that
130+
* Liquibase’s normal precedence still applies (env variables and jvm system
131+
* properties can override values set here).
132+
*/
133+
private @Nullable Map<String, String> properties;
134+
126135
/**
127136
* File to which rollback SQL is written when an update is performed.
128137
*/
@@ -294,6 +303,14 @@ public void setParameters(@Nullable Map<String, String> parameters) {
294303
this.parameters = parameters;
295304
}
296305

306+
public @Nullable Map<String, String> getProperties() {
307+
return this.properties;
308+
}
309+
310+
public void setProperties(@Nullable Map<String, String> properties) {
311+
this.properties = properties;
312+
}
313+
297314
public @Nullable File getRollbackFile() {
298315
return this.rollbackFile;
299316
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.liquibase.autoconfigure;
18+
19+
import liquibase.configuration.ProvidedValue;
20+
import org.junit.jupiter.api.Test;
21+
import org.springframework.mock.env.MockEnvironment;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
class EnvironmentConfigurationValueProviderTests {
26+
27+
@Test
28+
void precedenceIsBetweenDefaultsAndEnvVars() {
29+
var env = new MockEnvironment();
30+
var provider = new EnvironmentConfigurationValueProvider(env);
31+
assertThat(provider.getPrecedence()).isEqualTo(100);
32+
}
33+
34+
@Test
35+
void returnsProvidedValueWhenExactPropertyPresent() {
36+
var env = new MockEnvironment().withProperty("spring.liquibase.properties.liquibase.duplicateFileMode", "WARN");
37+
var provider = new EnvironmentConfigurationValueProvider(env);
38+
39+
ProvidedValue value = provider.getProvidedValue("liquibase.duplicateFileMode");
40+
41+
assertThat(value).isNotNull();
42+
assertThat(String.valueOf(value.getValue())).isEqualTo("WARN");
43+
assertThat(value.getActualKey()).isEqualTo("liquibase.duplicateFileMode");
44+
assertThat(value.getSourceDescription())
45+
.contains("Spring Environment property 'spring.liquibase.properties.liquibase.duplicateFileMode'");
46+
}
47+
48+
@Test
49+
void returnsNullWhenNoKeysProvided() {
50+
var env = new MockEnvironment();
51+
var provider = new EnvironmentConfigurationValueProvider(env);
52+
53+
ProvidedValue value = provider.getProvidedValue((String[]) null);
54+
55+
assertThat(value).isNull();
56+
}
57+
58+
@Test
59+
void returnsNullWhenNoMatchingProperty() {
60+
var env = new MockEnvironment();
61+
var provider = new EnvironmentConfigurationValueProvider(env);
62+
63+
ProvidedValue value = provider.getProvidedValue("liquibase.searchPath");
64+
65+
assertThat(value).isNull();
66+
}
67+
68+
@Test
69+
void skipsNullKeysAndResolvesFirstMatchingNonNull() {
70+
var env = new MockEnvironment()
71+
// Only the second alias is present
72+
.withProperty("spring.liquibase.properties.liquibase.searchPath", "classpath:/db");
73+
var provider = new EnvironmentConfigurationValueProvider(env);
74+
75+
ProvidedValue value = provider.getProvidedValue(null, "liquibase.searchPath");
76+
77+
assertThat(value).isNotNull();
78+
assertThat(String.valueOf(value.getValue())).isEqualTo("classpath:/db");
79+
assertThat(value.getActualKey()).isEqualTo("liquibase.searchPath");
80+
}
81+
82+
@Test
83+
void doesNotApplyRelaxedBinding_exactKeyOnly() {
84+
var env = new MockEnvironment().withProperty("spring.liquibase.properties.liquibase.duplicate-file-mode",
85+
"WARN");
86+
var provider = new EnvironmentConfigurationValueProvider(env);
87+
88+
// Request camelCase; should not match the kebab-case property
89+
ProvidedValue value = provider.getProvidedValue("liquibase.duplicateFileMode");
90+
91+
assertThat(value).isNull();
92+
}
93+
94+
}

module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,92 @@ void shouldRegisterHints() {
563563
assertThat(RuntimeHintsPredicates.resource().forResource("db/changelog/tables/init.sql")).accepts(hints);
564564
}
565565

566+
@Test
567+
@WithDbChangelogMasterYamlResource
568+
void springLiquibaseTakesPrecedenceOverLiquibaseDefaults() {
569+
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class)
570+
.withPropertyValues("spring.liquibase.properties.liquibase.duplicateFileMode=WARN")
571+
.run((context) -> {
572+
assertThat(context).hasSingleBean(SpringLiquibase.class);
573+
574+
var liquibaseConfig = liquibase.Scope.getCurrentScope()
575+
.getSingleton(liquibase.configuration.LiquibaseConfiguration.class);
576+
577+
var provider = liquibaseConfig.getProviders()
578+
.stream()
579+
.filter(p -> p.getClass()
580+
.getName()
581+
.equals("org.springframework.boot.liquibase.autoconfigure.EnvironmentConfigurationValueProvider"))
582+
.findFirst()
583+
.orElseThrow();
584+
585+
var provided = provider.getProvidedValue("liquibase.duplicateFileMode");
586+
assertThat(provided).isNotNull();
587+
assertThat(String.valueOf(provided.getValue())).isEqualTo("WARN");
588+
589+
// Now check the resolved value, which should be different from the
590+
// default 'ERROR'
591+
var resolved = liquibaseConfig.getCurrentConfiguredValue(null, null, "liquibase.duplicateFileMode");
592+
assertThat(String.valueOf(resolved.getValue())).isEqualTo("WARN");
593+
});
594+
}
595+
596+
@Test
597+
@WithDbChangelogMasterYamlResource
598+
void systemPropertyTakesPrecedenceOverSpringLiquibaseProperties() {
599+
this.contextRunner.withSystemProperties("liquibase.duplicateFileMode=ERROR")
600+
.withUserConfiguration(EmbeddedDataSourceConfiguration.class)
601+
.withPropertyValues("spring.liquibase.properties.liquibase.duplicateFileMode=WARN")
602+
.run((context) -> {
603+
assertThat(context).hasSingleBean(SpringLiquibase.class);
604+
605+
var liquibaseConfig = liquibase.Scope.getCurrentScope()
606+
.getSingleton(liquibase.configuration.LiquibaseConfiguration.class);
607+
608+
var provider = liquibaseConfig.getProviders()
609+
.stream()
610+
.filter(p -> p.getClass()
611+
.getName()
612+
.equals("org.springframework.boot.liquibase.autoconfigure.EnvironmentConfigurationValueProvider"))
613+
.findFirst()
614+
.orElseThrow();
615+
616+
// Our provider should return the value set through Spring property
617+
var providedBySpring = provider.getProvidedValue("liquibase.duplicateFileMode");
618+
assertThat(providedBySpring).isNotNull();
619+
assertThat(String.valueOf(providedBySpring.getValue())).isEqualTo("WARN");
620+
621+
// Now check the resolved value, which should be the system property
622+
var resolved = liquibaseConfig.getCurrentConfiguredValue(null, null, "liquibase.duplicateFileMode");
623+
assertThat(String.valueOf(resolved.getValue())).isEqualTo("ERROR");
624+
});
625+
}
626+
627+
@Test
628+
@WithDbChangelogMasterYamlResource
629+
void arbitraryLiquibaseKeyIsPassedThroughAsIs() {
630+
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class)
631+
.withPropertyValues("spring.liquibase.properties.my.extension.custom.option=true")
632+
.run((context) -> {
633+
assertThat(context).hasSingleBean(SpringLiquibase.class);
634+
635+
var liquibaseConfig = liquibase.Scope.getCurrentScope()
636+
.getSingleton(liquibase.configuration.LiquibaseConfiguration.class);
637+
638+
var provider = liquibaseConfig.getProviders()
639+
.stream()
640+
.filter(p -> p.getClass()
641+
.getName()
642+
.equals("org.springframework.boot.liquibase.autoconfigure.EnvironmentConfigurationValueProvider"))
643+
.findFirst()
644+
.orElseThrow();
645+
646+
var provided = provider.getProvidedValue("my.extension.custom.option");
647+
assertThat(provided).isNotNull();
648+
assertThat(String.valueOf(provided.getValue())).isEqualTo("true");
649+
});
650+
}
651+
566652
@Test
567653
@WithDbChangelogMasterYamlResource
568654
void whenCustomizerBeanIsDefinedThenItIsConfiguredOnSpringLiquibase() {

0 commit comments

Comments
 (0)