From a1443d1cdc7faa85b47507452139c17940b42f30 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2022 12:07:16 +0100 Subject: [PATCH] Enforce ordering in additional-spring-configuration-metadata.json files Closes gh-31575 --- ...AdditionalSpringConfigurationMetadata.java | 166 ++++++++++++++++++ .../ConfigurationPropertiesPlugin.java | 28 ++- ...itional-spring-configuration-metadata.json | 8 +- 3 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java 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 new file mode 100644 index 0000000000..b95e8e0a42 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2022 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.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 java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileTree; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.InputFiles; +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; + +/** + * {@link SourceTask} that checks additional Spring configuration metadata files. + * + * @author Andy Wilkinson + */ +public class CheckAdditionalSpringConfigurationMetadata extends SourceTask { + + private final RegularFileProperty reportLocation; + + public CheckAdditionalSpringConfigurationMetadata() { + this.reportLocation = getProject().getObjects().fileProperty(); + } + + @OutputFile + public RegularFileProperty getReportLocation() { + return this.reportLocation; + } + + @Override + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public FileTree getSource() { + return super.getSource(); + } + + @TaskAction + void check() throws JsonParseException, IOException { + Report report = createReport(); + File reportFile = getReportLocation().get().getAsFile(); + Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + if (report.hasProblems()) { + throw new GradleException( + "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(getProject().getProjectDir().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.get(key); + List names = groups.stream().map((group) -> (String) group.get("name")).collect(Collectors.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/ConfigurationPropertiesPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesPlugin.java index 7eefaa20b8..4f8c5db265 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -29,7 +29,9 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.language.base.plugins.LifecycleBasePlugin; import org.springframework.util.StringUtils; @@ -43,6 +45,8 @@ import org.springframework.util.StringUtils; * argument. *
  • Adding the outputs of the processResources task as inputs of the compileJava task * to ensure that the additional metadata is available when the annotation processor runs. + *
  • Registering a {@link CheckAdditionalSpringConfigurationMetadata} task and + * configuring the {@code check} task to depend upon it. *
  • Defining an artifact for the resulting configuration property metadata so that it * can be consumed by downstream projects. * @@ -57,11 +61,17 @@ public class ConfigurationPropertiesPlugin implements Plugin { */ public static final String CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME = "configurationPropertiesMetadata"; + /** + * Name of the {@link CheckAdditionalSpringConfigurationMetadata} task. + */ + public static final String CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkAdditionalSpringConfigurationMetadata"; + @Override public void apply(Project project) { project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { addConfigurationProcessorDependency(project); configureAdditionalMetadataLocationsCompilerArgument(project); + registerCheckAdditionalMetadataTask(project); addMetadataArtifact(project); }); } @@ -98,4 +108,20 @@ public class ConfigurationPropertiesPlugin implements Plugin { .stream().map(project.getRootProject()::relativePath).collect(Collectors.toSet()))); } + private void registerCheckAdditionalMetadataTask(Project project) { + TaskProvider checkConfigurationMetadata = project.getTasks() + .register(CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME, + CheckAdditionalSpringConfigurationMetadata.class); + checkConfigurationMetadata.configure((check) -> { + SourceSet mainSourceSet = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + check.setSource(mainSourceSet.getResources()); + check.include("META-INF/additional-spring-configuration-metadata.json"); + check.getReportLocation().set(project.getLayout().getBuildDirectory() + .file("reports/additional-spring-configuration-metadata/check.txt")); + }); + project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME) + .configure((check) -> check.dependsOn(checkConfigurationMetadata)); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 90410a6891..6bcc5be7f6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -492,16 +492,16 @@ "name": "spring.data.cassandra.connection.init-query-timeout", "defaultValue": "5s" }, - { - "name": "spring.data.cassandra.controlconnection.timeout", - "defaultValue": "5s" - }, { "name": "spring.data.cassandra.contact-points", "defaultValue": [ "127.0.0.1:9042" ] }, + { + "name": "spring.data.cassandra.controlconnection.timeout", + "defaultValue": "5s" + }, { "name": "spring.data.cassandra.jmx-enabled", "type": "java.lang.Boolean",