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; + } + +}