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) 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); + } + +} diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-error-no-replacement.properties b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-error-no-replacement.properties new file mode 100644 index 0000000000..30819a0571 --- /dev/null +++ b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-error-no-replacement.properties @@ -0,0 +1,6 @@ +server.port=8080 + +debug=false + + +wrong.three=invalid diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-error.properties b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-error.properties new file mode 100644 index 0000000000..cec0fef0bf --- /dev/null +++ b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-error.properties @@ -0,0 +1,2 @@ + +wrong.one=test diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-warnings.properties b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-warnings.properties new file mode 100644 index 0000000000..e862c7d743 --- /dev/null +++ b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/config/config-warnings.properties @@ -0,0 +1,5 @@ + +wrong.two=another + + +wrong.four.test=value diff --git a/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/metadata/sample-metadata.json b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/metadata/sample-metadata.json new file mode 100644 index 0000000000..e7ac0c8d68 --- /dev/null +++ b/spring-boot-project/spring-boot-configuration-analyzer/src/test/resources/metadata/sample-metadata.json @@ -0,0 +1,41 @@ +{ + "properties": [ + { + "name": "test.two", + "type": "java.lang.String" + }, + { + "name": "test.four", + "type": "java.util.Map" + }, + { + "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" + } + } + ] +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-dependencies/pom.xml b/spring-boot-project/spring-boot-dependencies/pom.xml index 5f7b3d392e..05332fde03 100644 --- a/spring-boot-project/spring-boot-dependencies/pom.xml +++ b/spring-boot-project/spring-boot-dependencies/pom.xml @@ -239,6 +239,11 @@ spring-boot-autoconfigure-processor ${revision} + + org.springframework.boot + spring-boot-configuration-analyzer + ${revision} + org.springframework.boot spring-boot-configuration-metadata diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java index 5ffb63ff6a..ffa0a1b8e2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedMapPropertySource.java @@ -29,13 +29,14 @@ import org.springframework.core.env.MapPropertySource; * * @author Madhura Bhave * @author Phillip Webb + * @since 2.0.0 * @see OriginTrackedValue */ -class OriginTrackedMapPropertySource extends MapPropertySource +public final class OriginTrackedMapPropertySource extends MapPropertySource implements OriginLookup { @SuppressWarnings({ "unchecked", "rawtypes" }) - OriginTrackedMapPropertySource(String name, Map source) { + public OriginTrackedMapPropertySource(String name, Map source) { super(name, source); }