From 52951ed8f433002068dedd03df8eef22ad6eade7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 10 Nov 2025 15:22:13 +0100 Subject: [PATCH] Apply checks for manual configuration metadata This commit adds a 'org.springframework.boot.configuration-metadata' plugin to be used for projects that only define manual metadata. Such project do not need the annotation processor, but do not to check that the structure of the metadata content matches the same rules. Closes gh-47984 --- buildSrc/build.gradle | 4 + ...AdditionalSpringConfigurationMetadata.java | 107 +------- ...heckManualSpringConfigurationMetadata.java | 77 ++++++ .../CheckSpringConfigurationMetadata.java | 98 +------ .../ConfigurationMetadataPlugin.java | 76 ++++++ .../ConfigurationPropertiesAnalyzer.java | 251 ++++++++++++++++++ .../ConfigurationPropertiesAnalyzerTests.java | 185 +++++++++++++ 7 files changed, 611 insertions(+), 187 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataPlugin.java create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f332283bcbc..12836c3a979 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -112,6 +112,10 @@ gradlePlugin { id = "org.springframework.boot.bom" implementationClass = "org.springframework.boot.build.bom.BomPlugin" } + configurationMetadataPlugin { + id = "org.springframework.boot.configuration-metadata" + implementationClass = "org.springframework.boot.build.context.properties.ConfigurationMetadataPlugin" + } configurationPropertiesPlugin { id = "org.springframework.boot.configuration-properties" implementationClass = "org.springframework.boot.build.context.properties.ConfigurationPropertiesPlugin" diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java index f2f1edec889..2ca87f20ae6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java @@ -18,19 +18,7 @@ package org.springframework.boot.build.context.properties; import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.gradle.api.file.FileTree; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.tasks.InputFiles; @@ -41,6 +29,8 @@ import org.gradle.api.tasks.SourceTask; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.VerificationException; +import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report; + /** * {@link SourceTask} that checks additional Spring configuration metadata files. * @@ -65,99 +55,16 @@ public abstract class CheckAdditionalSpringConfigurationMetadata extends SourceT } @TaskAction - void check() throws JsonParseException, IOException { - Report report = createReport(); + void check() throws IOException { + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(getSource().getFiles()); + Report report = new Report(this.projectDir); + analyzer.analyzeSort(report); File reportFile = getReportLocation().get().getAsFile(); - Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + report.write(reportFile); if (report.hasProblems()) { throw new VerificationException( "Problems found in additional Spring configuration metadata. See " + reportFile + " for details."); } } - @SuppressWarnings("unchecked") - private Report createReport() throws IOException, JsonParseException, JsonMappingException { - ObjectMapper objectMapper = new ObjectMapper(); - Report report = new Report(); - for (File file : getSource().getFiles()) { - Analysis analysis = report.analysis(this.projectDir.toPath().relativize(file.toPath())); - Map json = objectMapper.readValue(file, Map.class); - check("groups", json, analysis); - check("properties", json, analysis); - check("hints", json, analysis); - } - return report; - } - - @SuppressWarnings("unchecked") - private void check(String key, Map json, Analysis analysis) { - List> groups = (List>) json.getOrDefault(key, Collections.emptyList()); - List names = groups.stream().map((group) -> (String) group.get("name")).toList(); - List sortedNames = sortedCopy(names); - for (int i = 0; i < names.size(); i++) { - String actual = names.get(i); - String expected = sortedNames.get(i); - if (!actual.equals(expected)) { - analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected - + "' but found '" + actual + "'"); - } - } - } - - private List sortedCopy(Collection original) { - List copy = new ArrayList<>(original); - Collections.sort(copy); - return copy; - } - - private static final class Report implements Iterable { - - private final List analyses = new ArrayList<>(); - - private Analysis analysis(Path path) { - Analysis analysis = new Analysis(path); - this.analyses.add(analysis); - return analysis; - } - - private boolean hasProblems() { - for (Analysis analysis : this.analyses) { - if (!analysis.problems.isEmpty()) { - return true; - } - } - return false; - } - - @Override - public Iterator iterator() { - List lines = new ArrayList<>(); - for (Analysis analysis : this.analyses) { - lines.add(analysis.source.toString()); - lines.add(""); - if (analysis.problems.isEmpty()) { - lines.add("No problems found."); - } - else { - lines.addAll(analysis.problems); - } - lines.add(""); - } - return lines.iterator(); - } - - } - - private static final class Analysis { - - private final List problems = new ArrayList<>(); - - private final Path source; - - private Analysis(Path source) { - this.source = source; - } - - } - } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java new file mode 100644 index 00000000000..91ed37e1fb4 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckManualSpringConfigurationMetadata.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-present 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 + * + * https://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.build.context.properties; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceTask; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.VerificationException; + +import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report; + +/** + * {@link SourceTask} that checks manual Spring configuration metadata files. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + */ +public abstract class CheckManualSpringConfigurationMetadata extends DefaultTask { + + private final File projectDir; + + public CheckManualSpringConfigurationMetadata() { + this.projectDir = getProject().getProjectDir(); + } + + @OutputFile + public abstract RegularFileProperty getReportLocation(); + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract Property getMetadataLocation(); + + @Input + public abstract ListProperty getExclusions(); + + @TaskAction + void check() throws IOException { + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer( + List.of(getMetadataLocation().get())); + Report report = new Report(this.projectDir); + analyzer.analyzeSort(report); + analyzer.analyzePropertyDescription(report, getExclusions().get()); + File reportFile = getReportLocation().get().getAsFile(); + report.write(reportFile); + if (report.hasProblems()) { + throw new VerificationException( + "Problems found in manual Spring configuration metadata. See " + reportFile + " for details."); + } + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckSpringConfigurationMetadata.java index 7e86a41a8f8..55daea7ab9e 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckSpringConfigurationMetadata.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckSpringConfigurationMetadata.java @@ -18,17 +18,8 @@ package org.springframework.boot.build.context.properties; import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.Map; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.gradle.api.DefaultTask; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; @@ -41,6 +32,8 @@ import org.gradle.api.tasks.SourceTask; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.VerificationException; +import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report; + /** * {@link SourceTask} that checks {@code spring-configuration-metadata.json} files. * @@ -48,10 +41,10 @@ import org.gradle.api.tasks.VerificationException; */ public abstract class CheckSpringConfigurationMetadata extends DefaultTask { - private final Path projectRoot; + private final File projectRoot; public CheckSpringConfigurationMetadata() { - this.projectRoot = getProject().getProjectDir().toPath(); + this.projectRoot = getProject().getProjectDir(); } @OutputFile @@ -65,87 +58,18 @@ public abstract class CheckSpringConfigurationMetadata extends DefaultTask { public abstract ListProperty getExclusions(); @TaskAction - void check() throws JsonParseException, IOException { - Report report = createReport(); + void check() throws IOException { + Report report = new Report(this.projectRoot); + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer( + List.of(getMetadataLocation().get().getAsFile())); + analyzer.analyzePropertyDescription(report, getExclusions().get()); + File reportFile = getReportLocation().get().getAsFile(); - Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + report.write(reportFile); if (report.hasProblems()) { throw new VerificationException( "Problems found in Spring configuration metadata. See " + reportFile + " for details."); } } - @SuppressWarnings("unchecked") - private Report createReport() throws IOException, JsonParseException, JsonMappingException { - ObjectMapper objectMapper = new ObjectMapper(); - File file = getMetadataLocation().get().getAsFile(); - Report report = new Report(this.projectRoot.relativize(file.toPath())); - Map json = objectMapper.readValue(file, Map.class); - List> properties = (List>) json.get("properties"); - for (Map property : properties) { - String name = (String) property.get("name"); - if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(name)) { - report.propertiesWithNoDescription.add(name); - } - } - return report; - } - - private boolean isExcluded(String propertyName) { - for (String exclusion : getExclusions().get()) { - if (propertyName.equals(exclusion)) { - return true; - } - if (exclusion.endsWith(".*")) { - if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) { - return true; - } - } - } - return false; - } - - @SuppressWarnings("unchecked") - private boolean isDeprecated(Map property) { - return (Map) property.get("deprecation") != null; - } - - private boolean isDescribed(Map property) { - return property.get("description") != null; - } - - private static final class Report implements Iterable { - - private final List propertiesWithNoDescription = new ArrayList<>(); - - private final Path source; - - private Report(Path source) { - this.source = source; - } - - private boolean hasProblems() { - return !this.propertiesWithNoDescription.isEmpty(); - } - - @Override - public Iterator iterator() { - List lines = new ArrayList<>(); - lines.add(this.source.toString()); - lines.add(""); - if (this.propertiesWithNoDescription.isEmpty()) { - lines.add("No problems found."); - } - else { - lines.add("The following properties have no description:"); - lines.add(""); - lines.addAll(this.propertiesWithNoDescription.stream().map((line) -> "\t" + line).toList()); - } - lines.add(""); - return lines.iterator(); - - } - - } - } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataPlugin.java new file mode 100644 index 00000000000..ab56b285b02 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataPlugin.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present 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 + * + * https://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.build.context.properties; + +import java.io.File; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.base.plugins.LifecycleBasePlugin; +import org.gradle.language.jvm.tasks.ProcessResources; + +/** + * {@link Plugin} for projects that only define manual configuration metadata. + * When applied, the plugin registers a {@link CheckManualSpringConfigurationMetadata} + * task and configures the {@code check} task to depend upon it. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + */ +public class ConfigurationMetadataPlugin implements Plugin { + + /** + * Name of the {@link CheckAdditionalSpringConfigurationMetadata} task. + */ + public static final String CHECK_MANUAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkManualSpringConfigurationMetadata"; + + @Override + public void apply(Project project) { + project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerCheckAdditionalMetadataTask(project)); + } + + private void registerCheckAdditionalMetadataTask(Project project) { + TaskProvider checkConfigurationMetadata = project.getTasks() + .register(CHECK_MANUAL_SPRING_CONFIGURATION_METADATA_TASK_NAME, + CheckManualSpringConfigurationMetadata.class); + checkConfigurationMetadata.configure((check) -> { + SourceSet mainSourceSet = project.getExtensions() + .getByType(JavaPluginExtension.class) + .getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + + Provider manualMetadataLocation = project.getTasks() + .named(mainSourceSet.getProcessResourcesTaskName(), ProcessResources.class) + .map((processResources) -> new File(processResources.getDestinationDir(), + "META-INF/spring-configuration-metadata.json")); + check.getMetadataLocation().set(manualMetadataLocation); + check.getReportLocation() + .set(project.getLayout() + .getBuildDirectory() + .file("reports/manual-spring-configuration-metadata/check.txt")); + }); + project.getTasks() + .named(LifecycleBasePlugin.CHECK_TASK_NAME) + .configure((check) -> check.dependsOn(checkConfigurationMetadata)); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java new file mode 100644 index 00000000000..a456829fd36 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzer.java @@ -0,0 +1,251 @@ +/* + * Copyright 2012-present 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 + * + * https://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.build.context.properties; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.function.SingletonSupplier; + +/** + * Check configuration metadata for inconsistencies. The available checks are: + *
    + *
  • Metadata element should be sorted alphabetically: {@link #analyzeSort(Report)}
  • + *
  • Property must have a description: + * {@link #analyzePropertyDescription(Report, List)}
  • + *
+ * + * @author Stephane Nicoll + */ +class ConfigurationPropertiesAnalyzer { + + private final Collection sources; + + private final SingletonSupplier objectMapperSupplier; + + ConfigurationPropertiesAnalyzer(Collection sources) { + if (sources.isEmpty()) { + throw new IllegalArgumentException("At least one source should be provided"); + } + this.sources = sources; + this.objectMapperSupplier = SingletonSupplier.of(ObjectMapper::new); + } + + void analyzeSort(Report report) throws IOException { + for (File source : this.sources) { + report.registerAnalysis(source, analyzeSort(source)); + } + } + + private Analysis analyzeSort(File source) throws IOException { + Map json = readJsonContent(source); + Analysis analysis = new Analysis("Metadata element order:"); + analyzeMetadataElementsSort("groups", json, analysis); + analyzeMetadataElementsSort("properties", json, analysis); + analyzeMetadataElementsSort("hints", json, analysis); + return analysis; + } + + @SuppressWarnings("unchecked") + private void analyzeMetadataElementsSort(String key, Map json, Analysis analysis) { + List> groups = (List>) json.getOrDefault(key, Collections.emptyList()); + List names = groups.stream().map((group) -> (String) group.get("name")).toList(); + List sortedNames = names.stream().sorted().toList(); + for (int i = 0; i < names.size(); i++) { + String actual = names.get(i); + String expected = sortedNames.get(i); + if (!actual.equals(expected)) { + analysis.addItem("Wrong order at $." + key + "[" + i + "].name - expected '" + expected + + "' but found '" + actual + "'"); + } + } + } + + void analyzePropertyDescription(Report report, List exclusions) throws IOException { + for (File source : this.sources) { + report.registerAnalysis(source, analyzePropertyDescription(source, exclusions)); + } + } + + @SuppressWarnings("unchecked") + private Analysis analyzePropertyDescription(File source, List exclusions) throws IOException { + Map json = readJsonContent(source); + Analysis analysis = new Analysis("The following properties have no description:"); + List> properties = (List>) json.get("properties"); + for (Map property : properties) { + String name = (String) property.get("name"); + if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(exclusions, name)) { + analysis.addItem(name); + } + } + return analysis; + } + + private boolean isExcluded(List exclusions, String propertyName) { + for (String exclusion : exclusions) { + if (propertyName.equals(exclusion)) { + return true; + } + if (exclusion.endsWith(".*")) { + if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) { + return true; + } + } + } + return false; + } + + private boolean isDeprecated(Map property) { + return property.get("deprecation") != null; + } + + private boolean isDescribed(Map property) { + return property.get("description") != null; + } + + private Map readJsonContent(File source) throws IOException { + return this.objectMapperSupplier.obtain().readValue(source, new TypeReference>() { + }); + } + + private static void writeAll(PrintWriter writer, Iterable elements, Consumer itemWriter) { + Iterator it = elements.iterator(); + while (it.hasNext()) { + itemWriter.accept(it.next()); + if (it.hasNext()) { + writer.println(); + } + } + } + + static class Report { + + private final File baseDirectory; + + private final MultiValueMap analyses = new LinkedMultiValueMap<>(); + + Report(File baseDirectory) { + this.baseDirectory = baseDirectory; + } + + void registerAnalysis(File path, Analysis analysis) { + this.analyses.add(path, analysis); + } + + boolean hasProblems() { + return this.analyses.values() + .stream() + .anyMatch((candidates) -> candidates.stream().anyMatch(Analysis::hasProblems)); + } + + List getAnalyses(File source) { + return this.analyses.getOrDefault(source, Collections.emptyList()); + } + + /** + * Write this report to the given {@code file}. + * @param file the file to write the report to + */ + void write(File file) throws IOException { + Files.writeString(file.toPath(), createContent(), StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + } + + private String createContent() { + if (this.analyses.isEmpty()) { + return "No problems found."; + } + StringWriter out = new StringWriter(); + try (PrintWriter writer = new PrintWriter(out)) { + writeAll(writer, this.analyses.entrySet(), (entry) -> { + writer.println(this.baseDirectory.toPath().relativize(entry.getKey().toPath())); + boolean hasProblems = entry.getValue().stream().anyMatch(Analysis::hasProblems); + if (hasProblems) { + writeAll(writer, entry.getValue(), (analysis) -> analysis.createDetails(writer)); + } + else { + writer.println("No problems found."); + } + }); + } + return out.toString(); + } + + } + + static class Analysis { + + private final String header; + + private final List items; + + Analysis(String header) { + this.header = header; + this.items = new ArrayList<>(); + } + + void addItem(String item) { + this.items.add(item); + } + + boolean hasProblems() { + return !this.items.isEmpty(); + } + + List getItems() { + return this.items; + } + + void createDetails(PrintWriter writer) { + writer.println(this.header); + if (this.items.isEmpty()) { + writer.println("No problems found."); + } + else { + for (String item : this.items) { + writer.println("\t- " + item); + } + } + } + + @Override + public String toString() { + StringWriter out = new StringWriter(); + PrintWriter writer = new PrintWriter(out); + createDetails(writer); + return out.toString(); + } + + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java new file mode 100644 index 00000000000..2ee0df2122f --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesAnalyzerTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2012-present 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 + * + * https://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.build.context.properties; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Analysis; +import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ConfigurationPropertiesAnalyzer}. + * + * @author Stephane Nicoll + */ +class ConfigurationPropertiesAnalyzerTests { + + @Test + void createAnalyzerWithNoSource() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ConfigurationPropertiesAnalyzer(Collections.emptyList())) + .withMessage("At least one source should be provided"); + } + + @Test + void analyzeSortWithAlphabeticalOrder(@TempDir File tempDir) throws IOException { + File metadata = new File(tempDir, "metadata.json"); + Files.writeString(metadata.toPath(), """ + { "properties": [ + { "name": "abc"}, {"name": "def"}, {"name": "xyz"} + ] + }"""); + Report report = new Report(tempDir); + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); + analyzer.analyzeSort(report); + assertThat(report.hasProblems()).isFalse(); + assertThat(report.getAnalyses(metadata)).singleElement() + .satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty())); + } + + @Test + void analyzeSortWithViolations(@TempDir File tempDir) throws IOException { + File metadata = new File(tempDir, "metadata.json"); + Files.writeString(metadata.toPath(), """ + { "properties": [ + { "name": "def"}, {"name": "abc"}, {"name": "xyz"} + ] + }"""); + Report report = new Report(tempDir); + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); + analyzer.analyzeSort(report); + assertThat(report.hasProblems()).isTrue(); + assertThat(report.getAnalyses(metadata)).singleElement() + .satisfies((analysis) -> assertThat(analysis.getItems()).containsExactly( + "Wrong order at $.properties[0].name - expected 'abc' but found 'def'", + "Wrong order at $.properties[1].name - expected 'def' but found 'abc'")); + } + + @Test + void analyzePropertyDescription(@TempDir File tempDir) throws IOException { + File metadata = new File(tempDir, "metadata.json"); + Files.writeString(metadata.toPath(), """ + { "properties": [ + { "name": "abc", "description": "This is abc." }, + { "name": "def", "description": "This is def." }, + { "name": "xyz", "description": "This is xyz." } + ] + }"""); + Report report = new Report(tempDir); + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); + analyzer.analyzePropertyDescription(report, List.of()); + assertThat(report.hasProblems()).isFalse(); + assertThat(report.getAnalyses(metadata)).singleElement() + .satisfies(((analysis) -> assertThat(analysis.getItems()).isEmpty())); + } + + @Test + void analyzePropertyDescriptionWithMissingDescription(@TempDir File tempDir) throws IOException { + File metadata = new File(tempDir, "metadata.json"); + Files.writeString(metadata.toPath(), """ + { "properties": [ + { "name": "abc", "description": "This is abc." }, + { "name": "def" }, + { "name": "xyz", "description": "This is xyz." } + ] + }"""); + Report report = new Report(tempDir); + ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(List.of(metadata)); + analyzer.analyzePropertyDescription(report, List.of()); + assertThat(report.hasProblems()).isTrue(); + assertThat(report.getAnalyses(metadata)).singleElement() + .satisfies(((analysis) -> assertThat(analysis.getItems()).containsExactly("def"))); + } + + @Test + void writeEmptyReport(@TempDir File tempDir) throws IOException { + assertThat(writeToFile(tempDir, new Report(tempDir))).hasContent("No problems found."); + } + + @Test + void writeReportWithNoProblemsFound(@TempDir File tempDir) throws IOException { + Report report = new Report(tempDir); + File first = new File(tempDir, "metadata-1.json"); + report.registerAnalysis(first, new Analysis("Check for things:")); + File second = new File(tempDir, "metadata-2.json"); + report.registerAnalysis(second, new Analysis("Check for other things:")); + assertThat(writeToFile(tempDir, report)).content().isEqualTo(""" + metadata-1.json + No problems found. + + metadata-2.json + No problems found. + """); + } + + @Test + void writeReportWithOneProblem(@TempDir File tempDir) throws IOException { + Report report = new Report(tempDir); + File metadata = new File(tempDir, "metadata-1.json"); + Analysis analysis = new Analysis("Check for things:"); + analysis.addItem("Should not be deprecated"); + report.registerAnalysis(metadata, analysis); + report.registerAnalysis(metadata, new Analysis("Check for other things:")); + assertThat(writeToFile(tempDir, report)).content().isEqualTo(""" + metadata-1.json + Check for things: + - Should not be deprecated + + Check for other things: + No problems found. + """); + } + + @Test + void writeReportWithSeveralProblems(@TempDir File tempDir) throws IOException { + Report report = new Report(tempDir); + File metadata = new File(tempDir, "metadata-1.json"); + Analysis firstAnalysis = new Analysis("Check for things:"); + firstAnalysis.addItem("Should not be deprecated"); + firstAnalysis.addItem("Should not be public"); + report.registerAnalysis(metadata, firstAnalysis); + Analysis secondAnalysis = new Analysis("Check for other things:"); + secondAnalysis.addItem("Field 'this' not expected"); + report.registerAnalysis(metadata, secondAnalysis); + assertThat(writeToFile(tempDir, report)).content().isEqualTo(""" + metadata-1.json + Check for things: + - Should not be deprecated + - Should not be public + + Check for other things: + - Field 'this' not expected + """); + } + + private File writeToFile(File directory, Report report) throws IOException { + File file = new File(directory, "report.txt"); + report.write(file); + return file; + } + +}