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-11301pull/11724/head
parent
93168ea0dd
commit
6aa639253a
@ -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,6 @@
|
|||||||
|
server.port=8080
|
||||||
|
|
||||||
|
debug=false
|
||||||
|
|
||||||
|
|
||||||
|
wrong.three=invalid
|
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
wrong.one=test
|
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
wrong.two=another
|
||||||
|
|
||||||
|
|
||||||
|
wrong.four.test=value
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue