mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-27 12:39:03 +00:00
Merge branch '3.5.x'
Closes gh-48071
This commit is contained in:
@@ -114,6 +114,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"
|
||||
|
||||
+6
-98
@@ -18,15 +18,6 @@ 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 org.gradle.api.file.FileTree;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
@@ -37,8 +28,8 @@ 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 tools.jackson.core.StreamReadFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
|
||||
|
||||
/**
|
||||
* {@link SourceTask} that checks additional Spring configuration metadata files.
|
||||
@@ -65,98 +56,15 @@ public abstract class CheckAdditionalSpringConfigurationMetadata extends SourceT
|
||||
|
||||
@TaskAction
|
||||
void check() throws IOException {
|
||||
Report report = createReport();
|
||||
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() {
|
||||
JsonMapper jsonMapper = JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build();
|
||||
Report report = new Report();
|
||||
for (File file : getSource().getFiles()) {
|
||||
Analysis analysis = report.analysis(this.projectDir.toPath().relativize(file.toPath()));
|
||||
Map<String, Object> json = jsonMapper.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<String, Object> json, Analysis analysis) {
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.getOrDefault(key, Collections.emptyList());
|
||||
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
|
||||
List<String> 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<String> sortedCopy(Collection<String> original) {
|
||||
List<String> copy = new ArrayList<>(original);
|
||||
Collections.sort(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static final class Report implements Iterable<String> {
|
||||
|
||||
private final List<Analysis> 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<String> iterator() {
|
||||
List<String> 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<String> problems = new ArrayList<>();
|
||||
|
||||
private final Path source;
|
||||
|
||||
private Analysis(Path source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+77
@@ -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<File> getMetadataLocation();
|
||||
|
||||
@Input
|
||||
public abstract ListProperty<String> 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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+9
-84
@@ -18,13 +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.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
@@ -37,7 +31,8 @@ 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 tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import org.springframework.boot.build.context.properties.ConfigurationPropertiesAnalyzer.Report;
|
||||
|
||||
/**
|
||||
* {@link SourceTask} that checks {@code spring-configuration-metadata.json} files.
|
||||
@@ -46,10 +41,10 @@ import tools.jackson.databind.json.JsonMapper;
|
||||
*/
|
||||
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
|
||||
@@ -64,86 +59,16 @@ public abstract class CheckSpringConfigurationMetadata extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
void check() throws IOException {
|
||||
Report report = createReport();
|
||||
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() {
|
||||
JsonMapper jsonMapper = new JsonMapper();
|
||||
File file = getMetadataLocation().get().getAsFile();
|
||||
Report report = new Report(this.projectRoot.relativize(file.toPath()));
|
||||
Map<String, Object> json = jsonMapper.readValue(file, Map.class);
|
||||
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
|
||||
for (Map<String, Object> 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<String, Object> property) {
|
||||
return (Map<String, Object>) property.get("deprecation") != null;
|
||||
}
|
||||
|
||||
private boolean isDescribed(Map<String, Object> property) {
|
||||
return property.get("description") != null;
|
||||
}
|
||||
|
||||
private static final class Report implements Iterable<String> {
|
||||
|
||||
private final List<String> propertiesWithNoDescription = new ArrayList<>();
|
||||
|
||||
private final Path source;
|
||||
|
||||
private Report(Path source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
private boolean hasProblems() {
|
||||
return !this.propertiesWithNoDescription.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
List<String> 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+76
@@ -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 <em>only</em> 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<Project> {
|
||||
|
||||
/**
|
||||
* 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<CheckManualSpringConfigurationMetadata> 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<File> 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));
|
||||
}
|
||||
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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 tools.jackson.core.StreamReadFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
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:
|
||||
* <ul>
|
||||
* <li>Metadata element should be sorted alphabetically: {@link #analyzeSort(Report)}</li>
|
||||
* <li>Property must have a description:
|
||||
* {@link #analyzePropertyDescription(Report, List)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ConfigurationPropertiesAnalyzer {
|
||||
|
||||
private final Collection<File> sources;
|
||||
|
||||
private final SingletonSupplier<JsonMapper> jsonMapperSupplier;
|
||||
|
||||
ConfigurationPropertiesAnalyzer(Collection<File> sources) {
|
||||
if (sources.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one source should be provided");
|
||||
}
|
||||
this.sources = sources;
|
||||
this.jsonMapperSupplier = SingletonSupplier
|
||||
.of(() -> JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build());
|
||||
}
|
||||
|
||||
void analyzeSort(Report report) {
|
||||
for (File source : this.sources) {
|
||||
report.registerAnalysis(source, analyzeSort(source));
|
||||
}
|
||||
}
|
||||
|
||||
private Analysis analyzeSort(File source) {
|
||||
Map<String, Object> 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<String, Object> json, Analysis analysis) {
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.getOrDefault(key, Collections.emptyList());
|
||||
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
|
||||
List<String> 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<String> exclusions) {
|
||||
for (File source : this.sources) {
|
||||
report.registerAnalysis(source, analyzePropertyDescription(source, exclusions));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Analysis analyzePropertyDescription(File source, List<String> exclusions) {
|
||||
Map<String, Object> json = readJsonContent(source);
|
||||
Analysis analysis = new Analysis("The following properties have no description:");
|
||||
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
|
||||
for (Map<String, Object> 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<String> 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<String, Object> property) {
|
||||
return property.get("deprecation") != null;
|
||||
}
|
||||
|
||||
private boolean isDescribed(Map<String, Object> property) {
|
||||
return property.get("description") != null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readJsonContent(File source) {
|
||||
return this.jsonMapperSupplier.obtain().readValue(source, Map.class);
|
||||
}
|
||||
|
||||
private static <T> void writeAll(PrintWriter writer, Iterable<T> elements, Consumer<T> itemWriter) {
|
||||
Iterator<T> 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<File, Analysis> 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<Analysis> 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<String> 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<String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+185
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.configuration-metadata"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.configuration-metadata"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
id "org.springframework.boot.test-auto-configuration"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.configuration-metadata"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
id "org.springframework.boot.test-slice"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.configuration-metadata"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
id "org.springframework.boot.test-slice"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.configuration-metadata"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
id "org.springframework.boot.test-slice"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.springframework.boot.configuration-metadata"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
id "org.springframework.boot.test-slice"
|
||||
|
||||
Reference in New Issue
Block a user