Analyse the environment for properties that are no longer supported

This commit adds a new `spring-boot-configuration-analyzer` module that
can be added to any app to analyze its environment on startup.

Each configuration key that has a matching replacement is temporarily
transitioned to the new name with a `WARN` report that lists all of
them.

If the project defines configuration keys that don't have a replacement,
an `ERROR` report lists them with more information if it is available.

Closes gh-11301
pull/11724/head
Stephane Nicoll 7 years ago
parent 93168ea0dd
commit 6aa639253a

@ -21,6 +21,7 @@
<module>spring-boot-actuator</module> <module>spring-boot-actuator</module>
<module>spring-boot-actuator-autoconfigure</module> <module>spring-boot-actuator-autoconfigure</module>
<module>spring-boot-autoconfigure</module> <module>spring-boot-autoconfigure</module>
<module>spring-boot-configuration-analyzer</module>
<module>spring-boot-devtools</module> <module>spring-boot-devtools</module>
<module>spring-boot-test</module> <module>spring-boot-test</module>
<module>spring-boot-test-autoconfigure</module> <module>spring-boot-test-autoconfigure</module>

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>${revision}</version>
<relativePath>../spring-boot-parent</relativePath>
</parent>
<artifactId>spring-boot-configuration-analyzer</artifactId>
<name>Spring Boot Configuration Analyzer</name>
<description>Spring Boot Configuration Analyzer</description>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-metadata</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -0,0 +1,141 @@
/*
* Copyright 2012-2018 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
*
* http://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.configurationalayzer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.util.StringUtils;
/**
* Describes the outcome of the environment analysis.
*
* @author Stephane Nicoll
*/
class LegacyPropertiesAnalysis {
private final Map<String, PropertySourceAnalysis> content = new LinkedHashMap<>();
/**
* Create a report for all the legacy properties that were automatically renamed. If
* no such legacy properties were found, return {@code null}.
* @return a report with the configurations keys that should be renamed
*/
public String createWarningReport() {
Map<String, List<LegacyProperty>> content = this.content.entrySet().stream()
.filter(e -> !e.getValue().handledProperties.isEmpty())
.collect(Collectors.toMap(Map.Entry::getKey,
e -> new ArrayList<>(e.getValue().handledProperties)));
if (content.isEmpty()) {
return null;
}
StringBuilder report = new StringBuilder();
report.append(String.format("%nThe use of configuration keys that have been "
+ "renamed was found in the environment:%n%n"));
appendProperties(report, content, metadata ->
"Replacement: " + metadata.getDeprecation().getReplacement());
report.append(String.format("%n"));
report.append("Each configuration key has been temporarily mapped to its "
+ "replacement for your convenience. To silence this warning, please "
+ "update your configuration to use the new keys.");
report.append(String.format("%n"));
return report.toString();
}
/**
* Create a report for all the legacy properties that are no longer supported. If
* no such legacy properties were found, return {@code null}.
* @return a report with the configurations keys that are no longer supported
*/
public String createErrorReport() {
Map<String, List<LegacyProperty>> content = this.content.entrySet().stream()
.filter(e -> !e.getValue().notHandledProperties.isEmpty())
.collect(Collectors.toMap(Map.Entry::getKey,
e -> new ArrayList<>(e.getValue().notHandledProperties)));
if (content.isEmpty()) {
return null;
}
StringBuilder report = new StringBuilder();
report.append(String.format("%nThe use of configuration keys that are no longer "
+ "supported was found in the environment:%n%n"));
appendProperties(report, content, metadata ->
"Reason: " + (StringUtils.hasText(metadata.getDeprecation().getReason())
? metadata.getDeprecation().getReason() : "none"));
report.append(String.format("%n"));
report.append("Please refer to the migration guide or reference guide for "
+ "potential alternatives.");
report.append(String.format("%n"));
return report.toString();
}
private void appendProperties(StringBuilder report,
Map<String, List<LegacyProperty>> content,
Function<ConfigurationMetadataProperty, String> deprecationMessage) {
content.forEach((name, properties) -> {
report.append(String.format("Property source '%s':%n", name));
properties.sort(LegacyProperty.COMPARATOR);
properties.forEach((property) -> {
ConfigurationMetadataProperty metadata = property.getMetadata();
report.append(String.format("\tKey: %s%n", metadata.getId()));
if (property.getLineNumber() != null) {
report.append(String.format("\t\tLine: %d%n",
property.getLineNumber()));
}
report.append(String.format("\t\t%s%n",
deprecationMessage.apply(metadata)));
});
report.append(String.format("%n"));
});
}
/**
* Register a new property source.
* @param name the name of the property source
* @param handledProperties the properties that were renamed
* @param notHandledProperties the properties that are no longer supported
*/
void register(String name, List<LegacyProperty> handledProperties,
List<LegacyProperty> notHandledProperties) {
List<LegacyProperty> handled = (handledProperties != null
? new ArrayList<>(handledProperties) : Collections.emptyList());
List<LegacyProperty> notHandled = (notHandledProperties != null
? new ArrayList<>(notHandledProperties) : Collections.emptyList());
this.content.put(name, new PropertySourceAnalysis(handled, notHandled));
}
private static class PropertySourceAnalysis {
private final List<LegacyProperty> handledProperties;
private final List<LegacyProperty> notHandledProperties;
PropertySourceAnalysis(List<LegacyProperty> handledProperties,
List<LegacyProperty> notHandledProperties) {
this.handledProperties = handledProperties;
this.notHandledProperties = notHandledProperties;
}
}
}

@ -0,0 +1,169 @@
/*
* Copyright 2012-2018 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
*
* http://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.configurationalayzer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository;
import org.springframework.boot.configurationmetadata.Deprecation;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Analyse {@link LegacyProperty legacy properties}.
*
* @author Stephane Nicoll
*/
class LegacyPropertiesAnalyzer {
private final Map<String, ConfigurationMetadataProperty> allProperties;
private final ConfigurableEnvironment environment;
LegacyPropertiesAnalyzer(ConfigurationMetadataRepository metadataRepository,
ConfigurableEnvironment environment) {
this.allProperties = Collections.unmodifiableMap(metadataRepository.getAllProperties());
this.environment = environment;
}
/**
* Analyse the {@link ConfigurableEnvironment environment} and attempt to rename
* legacy properties if a replacement exists.
* @return the analysis
*/
public LegacyPropertiesAnalysis analyseLegacyProperties() {
LegacyPropertiesAnalysis analysis = new LegacyPropertiesAnalysis();
Map<String, List<LegacyProperty>> properties = getMatchingProperties(deprecatedFilter());
if (properties.isEmpty()) {
return analysis;
}
properties.forEach((name, candidates) -> {
PropertySource<?> propertySource = mapPropertiesWithReplacement(analysis,
name, candidates);
if (propertySource != null) {
this.environment.getPropertySources().addBefore(name, propertySource);
}
});
return analysis;
}
private PropertySource<?> mapPropertiesWithReplacement(
LegacyPropertiesAnalysis analysis, String name,
List<LegacyProperty> properties) {
List<LegacyProperty> matches = new ArrayList<>();
List<LegacyProperty> unhandled = new ArrayList<>();
for (LegacyProperty property : properties) {
if (hasValidReplacement(property)) {
matches.add(property);
}
else {
unhandled.add(property);
}
}
analysis.register(name, matches, unhandled);
if (matches.isEmpty()) {
return null;
}
String target = "migrate-" + name;
Map<String, OriginTrackedValue> content = new LinkedHashMap<>();
for (LegacyProperty candidate : matches) {
OriginTrackedValue value = OriginTrackedValue.of(
candidate.getProperty().getValue(), candidate.getProperty().getOrigin());
content.put(candidate.getMetadata().getDeprecation().getReplacement(), value);
}
return new OriginTrackedMapPropertySource(target, content);
}
private boolean hasValidReplacement(LegacyProperty property) {
String replacementId = property.getMetadata().getDeprecation().getReplacement();
if (StringUtils.hasText(replacementId)) {
ConfigurationMetadataProperty replacement = this.allProperties.get(replacementId);
if (replacement != null) {
return replacement.getType().equals(property.getMetadata().getType());
}
replacement = getMapProperty(replacementId);
if (replacement != null) {
return replacement.getType().startsWith("java.util.Map")
&& replacement.getType().endsWith(property.getMetadata().getType() + ">");
}
}
return false;
}
private ConfigurationMetadataProperty getMapProperty(String fullId) {
int i = fullId.lastIndexOf('.');
if (i != -1) {
return this.allProperties.get(fullId.substring(0, i));
}
return null;
}
private Map<String, List<LegacyProperty>> getMatchingProperties(
Predicate<ConfigurationMetadataProperty> filter) {
MultiValueMap<String, LegacyProperty> result = new LinkedMultiValueMap<>();
List<ConfigurationMetadataProperty> candidates = this.allProperties.values()
.stream().filter(filter).collect(Collectors.toList());
getPropertySourcesAsMap().forEach((name, source) -> {
candidates.forEach(metadata -> {
ConfigurationProperty configurationProperty = source.getConfigurationProperty(
ConfigurationPropertyName.of(metadata.getId()));
if (configurationProperty != null) {
result.add(name, new LegacyProperty(metadata, configurationProperty));
}
});
});
return result;
}
private Predicate<ConfigurationMetadataProperty> deprecatedFilter() {
return p -> p.getDeprecation() != null
&& p.getDeprecation().getLevel() == Deprecation.Level.ERROR;
}
private Map<String, ConfigurationPropertySource> getPropertySourcesAsMap() {
Map<String, ConfigurationPropertySource> map = new LinkedHashMap<>();
ConfigurationPropertySources.get(this.environment);
for (ConfigurationPropertySource source : ConfigurationPropertySources.get(this.environment)) {
map.put(determinePropertySourceName(source), source);
}
return map;
}
private String determinePropertySourceName(ConfigurationPropertySource source) {
if (source.getUnderlyingSource() instanceof PropertySource) {
return ((PropertySource<?>) source.getUnderlyingSource()).getName();
}
return source.getUnderlyingSource().toString();
}
}

@ -0,0 +1,113 @@
/*
* Copyright 2012-2018 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
*
* http://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.configurationalayzer;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/**
* An {@link ApplicationListener} that inspects the {@link ConfigurableEnvironment
* environment} for legacy configuration keys. Automatically renames the keys that
* have a matching replacement and log a report of what was discovered.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class LegacyPropertiesAnalyzerListener
implements ApplicationListener<SpringApplicationEvent> {
private static final Log logger = LogFactory.getLog(LegacyPropertiesAnalyzerListener.class);
private LegacyPropertiesAnalysis analysis;
private boolean analysisLogged;
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
if (event instanceof ApplicationReadyEvent
|| event instanceof ApplicationFailedEvent) {
logLegacyPropertiesAnalysis();
}
}
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
ConfigurationMetadataRepository repository = loadRepository();
ConfigurableEnvironment environment =
event.getApplicationContext().getEnvironment();
LegacyPropertiesAnalyzer validator = new LegacyPropertiesAnalyzer(
repository, environment);
this.analysis = validator.analyseLegacyProperties();
}
private void logLegacyPropertiesAnalysis() {
if (this.analysis == null || this.analysisLogged) {
return;
}
String warningReport = this.analysis.createWarningReport();
String errorReport = this.analysis.createErrorReport();
if (warningReport != null) {
logger.warn(warningReport);
}
if (errorReport != null) {
logger.error(errorReport);
}
this.analysisLogged = true;
}
private ConfigurationMetadataRepository loadRepository() {
try {
ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create();
for (InputStream inputStream : getResources()) {
builder.withJsonResource(inputStream);
}
return builder.build();
}
catch (IOException ex) {
throw new IllegalStateException("Failed to load metadata", ex);
}
}
private List<InputStream> getResources() throws IOException {
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath*:/META-INF/spring-configuration-metadata.json");
List<InputStream> result = new ArrayList<>();
for (Resource resource : resources) {
result.add(resource.getInputStream());
}
return result;
}
}

@ -0,0 +1,80 @@
/*
* Copyright 2012-2018 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
*
* http://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.configurationalayzer;
import java.util.Comparator;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.TextResourceOrigin;
/**
* Description of a legacy property.
*
* @author Stephane Nicoll
*/
class LegacyProperty {
static final LegacyPropertyComparator COMPARATOR = new LegacyPropertyComparator();
private final ConfigurationMetadataProperty metadata;
private final ConfigurationProperty property;
private final Integer lineNumber;
LegacyProperty(ConfigurationMetadataProperty metadata,
ConfigurationProperty property) {
this.metadata = metadata;
this.property = property;
this.lineNumber = determineLineNumber(property);
}
private static Integer determineLineNumber(ConfigurationProperty property) {
Origin origin = property.getOrigin();
if (origin instanceof TextResourceOrigin) {
TextResourceOrigin textOrigin = (TextResourceOrigin) origin;
if (textOrigin.getLocation() != null) {
return textOrigin.getLocation().getLine() + 1;
}
}
return null;
}
public ConfigurationMetadataProperty getMetadata() {
return this.metadata;
}
public ConfigurationProperty getProperty() {
return this.property;
}
public Integer getLineNumber() {
return this.lineNumber;
}
private static class LegacyPropertyComparator implements Comparator<LegacyProperty> {
@Override
public int compare(LegacyProperty p1, LegacyProperty p2) {
return p1.getMetadata().getId().compareTo(p2.getMetadata().getId());
}
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2018 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
*
* http://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.
*/
/**
* Support for analyzing the environment.
*/
package org.springframework.boot.configurationalayzer;

@ -0,0 +1,2 @@
org.springframework.context.ApplicationListener=\
org.springframework.boot.configurationalayzer.LegacyPropertiesAnalyzerListener

@ -0,0 +1,69 @@
/*
* Copyright 2012-2018 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
*
* http://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.configurationalayzer;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LegacyPropertiesAnalyzerListener}.
*
* @author Stephane Nicoll
*/
public class LegacyPropertiesAnalyzerListenerTests {
@Rule
public final OutputCapture output = new OutputCapture();
private ConfigurableApplicationContext context;
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void sampleReport() {
this.context = createSampleApplication()
.run("--banner.charset=UTF8");
assertThat(this.output.toString()).contains("commandLineArgs")
.contains("spring.banner.charset")
.contains("Each configuration key has been temporarily mapped")
.doesNotContain("Please refer to the migration guide");
}
private SpringApplication createSampleApplication() {
return new SpringApplication(TestApplication.class);
}
@Configuration
public static class TestApplication {
}
}

@ -0,0 +1,184 @@
/*
* Copyright 2012-2018 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
*
* http://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.configurationalayzer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder;
import org.springframework.boot.configurationmetadata.SimpleConfigurationMetadataRepository;
import org.springframework.boot.env.PropertiesPropertySourceLoader;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginLookup;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LegacyPropertiesAnalyzer}.
*
* @author Stephane Nicoll
*/
public class LegacyPropertiesAnalyzerTests {
private ConfigurableEnvironment environment = new MockEnvironment();
@Test
public void reportIsNullWithNoMatchingKeys() {
String report = createWarningReport(new SimpleConfigurationMetadataRepository());
assertThat(report).isNull();
}
@Test
public void replacementKeysAreRemapped() throws IOException {
MutablePropertySources propertySources = this.environment.getPropertySources();
PropertySource<?> one = loadPropertySource("one",
"config/config-error.properties");
PropertySource<?> two = loadPropertySource("two",
"config/config-warnings.properties");
propertySources.addFirst(one);
propertySources.addAfter("one", two);
assertThat(propertySources).hasSize(3);
createAnalyzer(loadRepository("metadata/sample-metadata.json"))
.analyseLegacyProperties();
assertThat(mapToNames(propertySources)).containsExactly("one",
"migrate-two", "two", "mockProperties");
assertMappedProperty(propertySources.get("migrate-two"),
"test.two", "another", getOrigin(two, "wrong.two"));
}
@Test
public void warningReport() throws IOException {
this.environment.getPropertySources().addFirst(loadPropertySource("test",
"config/config-warnings.properties"));
this.environment.getPropertySources().addFirst(loadPropertySource("ignore",
"config/config-error.properties"));
String report = createWarningReport(loadRepository(
"metadata/sample-metadata.json"));
assertThat(report).isNotNull();
assertThat(report).containsSubsequence("Property source 'test'",
"wrong.four.test", "Line: 5", "test.four.test",
"wrong.two", "Line: 2", "test.two");
assertThat(report).doesNotContain("wrong.one");
}
@Test
public void errorReport() throws IOException {
this.environment.getPropertySources().addFirst(loadPropertySource("test1",
"config/config-warnings.properties"));
this.environment.getPropertySources().addFirst(loadPropertySource("test2",
"config/config-error.properties"));
String report = createErrorReport(loadRepository(
"metadata/sample-metadata.json"));
assertThat(report).isNotNull();
assertThat(report).containsSubsequence("Property source 'test2'",
"wrong.one", "Line: 2", "This is no longer supported.");
assertThat(report).doesNotContain("wrong.four.test")
.doesNotContain("wrong.two");
}
@Test
public void errorReportNoReplacement() throws IOException {
this.environment.getPropertySources().addFirst(loadPropertySource("first",
"config/config-error-no-replacement.properties"));
this.environment.getPropertySources().addFirst(loadPropertySource("second",
"config/config-error.properties"));
String report = createErrorReport(loadRepository(
"metadata/sample-metadata.json"));
assertThat(report).isNotNull();
assertThat(report).containsSubsequence(
"Property source 'first'", "wrong.three", "Line: 6", "none",
"Property source 'second'", "wrong.one", "Line: 2",
"This is no longer supported.");
assertThat(report).doesNotContain("null").doesNotContain("server.port")
.doesNotContain("debug");
}
private List<String> mapToNames(PropertySources sources) {
List<String> names = new ArrayList<>();
for (PropertySource<?> source : sources) {
names.add(source.getName());
}
return names;
}
@SuppressWarnings("unchecked")
private Origin getOrigin(PropertySource<?> propertySource, String name) {
return ((OriginLookup<String>) propertySource).getOrigin(name);
}
private void assertMappedProperty(PropertySource<?> propertySource, String name,
Object value, Origin origin) {
assertThat(propertySource.containsProperty(name)).isTrue();
assertThat(propertySource.getProperty(name)).isEqualTo(value);
if (origin != null) {
assertThat(propertySource).isInstanceOf(OriginLookup.class);
assertThat(((OriginLookup<Object>) propertySource).getOrigin(name))
.isEqualTo(origin);
}
}
private PropertySource<?> loadPropertySource(String name, String path)
throws IOException {
ClassPathResource resource = new ClassPathResource(path);
PropertySource<?> propertySource = new PropertiesPropertySourceLoader()
.load(name, resource, null);
assertThat(propertySource).isNotNull();
return propertySource;
}
private ConfigurationMetadataRepository loadRepository(String... content) {
try {
ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create();
for (String path : content) {
Resource resource = new ClassPathResource(path);
builder.withJsonResource(resource.getInputStream());
}
return builder.build();
}
catch (IOException ex) {
throw new IllegalStateException("Failed to load metadata", ex);
}
}
private String createWarningReport(ConfigurationMetadataRepository repository) {
return createAnalyzer(repository).analyseLegacyProperties()
.createWarningReport();
}
private String createErrorReport(ConfigurationMetadataRepository repository) {
return createAnalyzer(repository).analyseLegacyProperties()
.createErrorReport();
}
private LegacyPropertiesAnalyzer createAnalyzer(
ConfigurationMetadataRepository repository) {
return new LegacyPropertiesAnalyzer(repository, this.environment);
}
}

@ -0,0 +1,41 @@
{
"properties": [
{
"name": "test.two",
"type": "java.lang.String"
},
{
"name": "test.four",
"type": "java.util.Map<java.lang.String,java.lang.String>"
},
{
"name": "wrong.one",
"deprecation": {
"reason": "This is no longer supported.",
"level": "error"
}
},
{
"name": "wrong.two",
"type": "java.lang.String",
"deprecation": {
"replacement": "test.two",
"level": "error"
}
},
{
"name": "wrong.three",
"deprecation": {
"level": "error"
}
},
{
"name": "wrong.four.test",
"type": "java.lang.String",
"deprecation": {
"replacement": "test.four.test",
"level": "error"
}
}
]
}

@ -239,6 +239,11 @@
<artifactId>spring-boot-autoconfigure-processor</artifactId> <artifactId>spring-boot-autoconfigure-processor</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-analyzer</artifactId>
<version>${revision}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-metadata</artifactId> <artifactId>spring-boot-configuration-metadata</artifactId>

@ -29,13 +29,14 @@ import org.springframework.core.env.MapPropertySource;
* *
* @author Madhura Bhave * @author Madhura Bhave
* @author Phillip Webb * @author Phillip Webb
* @since 2.0.0
* @see OriginTrackedValue * @see OriginTrackedValue
*/ */
class OriginTrackedMapPropertySource extends MapPropertySource public final class OriginTrackedMapPropertySource extends MapPropertySource
implements OriginLookup<String> { implements OriginLookup<String> {
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" })
OriginTrackedMapPropertySource(String name, Map source) { public OriginTrackedMapPropertySource(String name, Map source) {
super(name, source); super(name, source);
} }

Loading…
Cancel
Save