diff --git a/spring-boot-project/pom.xml b/spring-boot-project/pom.xml
index 209a82aa07..d770d9c363 100644
--- a/spring-boot-project/pom.xml
+++ b/spring-boot-project/pom.xml
@@ -21,6 +21,7 @@
spring-boot-actuator
spring-boot-actuator-autoconfigure
spring-boot-autoconfigure
+ spring-boot-configuration-analyzer
spring-boot-devtools
spring-boot-test
spring-boot-test-autoconfigure
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/pom.xml b/spring-boot-project/spring-boot-configuration-analyzer/pom.xml
new file mode 100644
index 0000000000..9058256c78
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-parent
+ ${revision}
+ ../spring-boot-parent
+
+ spring-boot-configuration-analyzer
+ Spring Boot Configuration Analyzer
+ Spring Boot Configuration Analyzer
+
+ ${basedir}/../..
+
+
+
+
+ org.springframework.boot
+ spring-boot
+
+
+ org.springframework.boot
+ spring-boot-configuration-metadata
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalysis.java b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalysis.java
new file mode 100644
index 0000000000..0d52564e78
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalysis.java
@@ -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 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> 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> 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> content,
+ Function 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 handledProperties,
+ List notHandledProperties) {
+ List handled = (handledProperties != null
+ ? new ArrayList<>(handledProperties) : Collections.emptyList());
+ List notHandled = (notHandledProperties != null
+ ? new ArrayList<>(notHandledProperties) : Collections.emptyList());
+ this.content.put(name, new PropertySourceAnalysis(handled, notHandled));
+ }
+
+
+ private static class PropertySourceAnalysis {
+
+ private final List handledProperties;
+
+ private final List notHandledProperties;
+
+ PropertySourceAnalysis(List handledProperties,
+ List notHandledProperties) {
+ this.handledProperties = handledProperties;
+ this.notHandledProperties = notHandledProperties;
+ }
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzer.java b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzer.java
new file mode 100644
index 0000000000..2b2518d19f
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzer.java
@@ -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 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> 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 properties) {
+ List matches = new ArrayList<>();
+ List 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 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> getMatchingProperties(
+ Predicate filter) {
+ MultiValueMap result = new LinkedMultiValueMap<>();
+ List 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 deprecatedFilter() {
+ return p -> p.getDeprecation() != null
+ && p.getDeprecation().getLevel() == Deprecation.Level.ERROR;
+ }
+
+ private Map getPropertySourcesAsMap() {
+ Map 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();
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerListener.java b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerListener.java
new file mode 100644
index 0000000000..e1aa1b79ac
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerListener.java
@@ -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 {
+
+ 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 getResources() throws IOException {
+ Resource[] resources = new PathMatchingResourcePatternResolver()
+ .getResources("classpath*:/META-INF/spring-configuration-metadata.json");
+ List result = new ArrayList<>();
+ for (Resource resource : resources) {
+ result.add(resource.getInputStream());
+ }
+ return result;
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyProperty.java b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyProperty.java
new file mode 100644
index 0000000000..29ca0e1216
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/LegacyProperty.java
@@ -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 {
+
+ @Override
+ public int compare(LegacyProperty p1, LegacyProperty p2) {
+ return p1.getMetadata().getId().compareTo(p2.getMetadata().getId());
+ }
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/package-info.java b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/package-info.java
new file mode 100644
index 0000000000..e5767382fc
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/main/java/org/springframework/boot/configurationalayzer/package-info.java
@@ -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;
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-configuration-analyzer/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..227d3af328
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.context.ApplicationListener=\
+org.springframework.boot.configurationalayzer.LegacyPropertiesAnalyzerListener
\ No newline at end of file
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/test/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerListenerTests.java b/spring-boot-project/spring-boot-configuration-analyzer/src/test/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerListenerTests.java
new file mode 100644
index 0000000000..5ec1532963
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/test/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerListenerTests.java
@@ -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 {
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/test/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerTests.java b/spring-boot-project/spring-boot-configuration-analyzer/src/test/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerTests.java
new file mode 100644
index 0000000000..f1ef2ba089
--- /dev/null
+++ b/spring-boot-project/spring-boot-configuration-analyzer/src/test/java/org/springframework/boot/configurationalayzer/LegacyPropertiesAnalyzerTests.java
@@ -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 mapToNames(PropertySources sources) {
+ List names = new ArrayList<>();
+ for (PropertySource> source : sources) {
+ names.add(source.getName());
+ }
+ return names;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Origin getOrigin(PropertySource> propertySource, String name) {
+ return ((OriginLookup) 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