diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java index 4a17cbec05..d4f35f7a72 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java @@ -82,6 +82,8 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; */ public class BomExtension { + private final String id; + private final Project project; private final UpgradeHandler upgradeHandler; @@ -95,6 +97,11 @@ public class BomExtension { public BomExtension(Project project) { this.project = project; this.upgradeHandler = project.getObjects().newInstance(UpgradeHandler.class, project); + this.id = "%s:%s:%s".formatted(project.getGroup(), project.getName(), project.getVersion()); + } + + public String getId() { + return this.id; } public List getLibraries() { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java index edb4154582..f6189567d6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java @@ -62,11 +62,17 @@ public class BomPlugin implements Plugin { javaPlatform.allowDependencies(); createApiEnforcedConfiguration(project); BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project); + TaskProvider createResolvedBom = project.getTasks() + .register("createResolvedBom", CreateResolvedBom.class, bom); TaskProvider checkBom = project.getTasks().register("bomrCheck", CheckBom.class, bom); project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom)); project.getTasks().register("bomrUpgrade", UpgradeBom.class, bom); project.getTasks().register("moveToSnapshots", MoveToSnapshots.class, bom); project.getTasks().register("checkLinks", CheckLinks.class, bom); + Configuration resolvedBomConfiguration = project.getConfigurations().create("resolvedBom"); + project.getArtifacts() + .add(resolvedBomConfiguration.getName(), createResolvedBom.map(CreateResolvedBom::getOutputFile), + (artifact) -> artifact.builtBy(createResolvedBom)); new PublishingCustomizer(project, bom).customize(); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomResolver.java new file mode 100644 index 0000000000..93b86c88a8 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomResolver.java @@ -0,0 +1,297 @@ +/* + * Copyright 2012-2025 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.bom; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +import org.springframework.boot.build.bom.Library.Group; +import org.springframework.boot.build.bom.Library.Module; +import org.springframework.boot.build.bom.ResolvedBom.Bom; +import org.springframework.boot.build.bom.ResolvedBom.Id; +import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary; + +/** + * Creates a {@link ResolvedBom resolved bom}. + * + * @author Andy Wilkinson + */ +class BomResolver { + + private final ConfigurationContainer configurations; + + private final DependencyHandler dependencies; + + private final DocumentBuilder documentBuilder; + + BomResolver(ConfigurationContainer configurations, DependencyHandler dependencies) { + this.configurations = configurations; + this.dependencies = dependencies; + try { + this.documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + } + catch (ParserConfigurationException ex) { + throw new RuntimeException(ex); + } + } + + ResolvedBom resolve(BomExtension bomExtension) { + List libraries = new ArrayList<>(); + for (Library library : bomExtension.getLibraries()) { + List managedDependencies = new ArrayList<>(); + List imports = new ArrayList<>(); + for (Group group : library.getGroups()) { + for (Module module : group.getModules()) { + Id id = new Id(group.getId(), module.getName(), library.getVersion().getVersion().toString()); + managedDependencies.add(id); + } + for (String imported : group.getBoms()) { + Bom bom = bomFrom(resolveBom( + "%s:%s:%s".formatted(group.getId(), imported, library.getVersion().getVersion()))); + imports.add(bom); + } + } + ResolvedLibrary resolvedLibrary = new ResolvedLibrary(library.getName(), + library.getVersion().getVersion().toString(), library.getVersionProperty(), managedDependencies, + imports); + libraries.add(resolvedLibrary); + } + String[] idComponents = bomExtension.getId().split(":"); + return new ResolvedBom(new Id(idComponents[0], idComponents[1], idComponents[2]), libraries); + } + + Bom resolveMavenBom(String coordinates) { + return bomFrom(resolveBom(coordinates)); + } + + private File resolveBom(String coordinates) { + Set artifacts = this.configurations + .detachedConfiguration(this.dependencies.create(coordinates + "@pom")) + .getResolvedConfiguration() + .getResolvedArtifacts(); + if (artifacts.size() != 1) { + throw new IllegalStateException("Expected a single artifact but '%s' resolved to %d artifacts" + .formatted(coordinates, artifacts.size())); + } + return artifacts.iterator().next().getFile(); + } + + private Bom bomFrom(File bomFile) { + try { + Node bom = nodeFrom(bomFile); + File parentBomFile = parentBomFile(bom); + Bom parent = null; + if (parentBomFile != null) { + parent = bomFrom(parentBomFile); + } + Properties properties = Properties.from(bom, this::nodeFrom); + List dependencyNodes = bom.nodesAt("/project/dependencyManagement/dependencies/dependency"); + List managedDependencies = new ArrayList<>(); + List imports = new ArrayList<>(); + for (Node dependency : dependencyNodes) { + String groupId = properties.replace(dependency.textAt("groupId")); + String artifactId = properties.replace(dependency.textAt("artifactId")); + String version = properties.replace(dependency.textAt("version")); + String classifier = properties.replace(dependency.textAt("classifier")); + String scope = properties.replace(dependency.textAt("scope")); + Bom importedBom = null; + if ("import".equals(scope)) { + String type = properties.replace(dependency.textAt("type")); + if ("pom".equals(type)) { + importedBom = bomFrom(resolveBom(groupId + ":" + artifactId + ":" + version)); + } + } + if (importedBom != null) { + imports.add(importedBom); + } + else { + managedDependencies.add(new Id(groupId, artifactId, version, classifier)); + } + } + String groupId = bom.textAt("/project/groupId"); + if ((groupId == null || groupId.isEmpty()) && parent != null) { + groupId = parent.id().groupId(); + } + String artifactId = bom.textAt("/project/artifactId"); + String version = bom.textAt("/project/version"); + if ((version == null || version.isEmpty()) && parent != null) { + version = parent.id().version(); + } + return new Bom(new Id(groupId, artifactId, version), parent, managedDependencies, imports); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private Node nodeFrom(String coordinates) { + return nodeFrom(resolveBom(coordinates)); + } + + private Node nodeFrom(File bomFile) { + try { + Document document = this.documentBuilder.parse(bomFile); + return new Node(document); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private File parentBomFile(Node bom) { + Node parent = bom.nodeAt("/project/parent"); + if (parent != null) { + String parentGroupId = parent.textAt("groupId"); + String parentArtifactId = parent.textAt("artifactId"); + String parentVersion = parent.textAt("version"); + return resolveBom(parentGroupId + ":" + parentArtifactId + ":" + parentVersion); + } + return null; + } + + private static final class Node { + + protected final XPath xpath; + + private final org.w3c.dom.Node delegate; + + private Node(org.w3c.dom.Node delegate) { + this(delegate, XPathFactory.newInstance().newXPath()); + } + + private Node(org.w3c.dom.Node delegate, XPath xpath) { + this.delegate = delegate; + this.xpath = xpath; + } + + private String textAt(String expression) { + String text = (String) evaluate(expression + "/text()", XPathConstants.STRING); + return (text != null && !text.isBlank()) ? text : null; + } + + private Node nodeAt(String expression) { + org.w3c.dom.Node result = (org.w3c.dom.Node) evaluate(expression, XPathConstants.NODE); + return (result != null) ? new Node(result, this.xpath) : null; + } + + private List nodesAt(String expression) { + NodeList nodes = (NodeList) evaluate(expression, XPathConstants.NODESET); + List things = new ArrayList<>(nodes.getLength()); + for (int i = 0; i < nodes.getLength(); i++) { + things.add(new Node(nodes.item(i), this.xpath)); + } + return things; + } + + private Object evaluate(String expression, QName type) { + try { + return this.xpath.evaluate(expression, this.delegate, type); + } + catch (XPathExpressionException ex) { + throw new RuntimeException(ex); + } + } + + private String name() { + return this.delegate.getNodeName(); + } + + private String textContent() { + return this.delegate.getTextContent(); + } + + } + + private static final class Properties { + + private final Map properties; + + private Properties(Map properties) { + this.properties = properties; + } + + private static Properties from(Node bom, Function resolver) { + try { + Map properties = new HashMap<>(); + Node current = bom; + while (current != null) { + String groupId = current.textAt("/project/groupId"); + if (groupId != null && !groupId.isEmpty()) { + properties.putIfAbsent("${project.groupId}", groupId); + } + String version = current.textAt("/project/version"); + if (version != null && !version.isEmpty()) { + properties.putIfAbsent("${project.version}", version); + } + List propertyNodes = current.nodesAt("/project/properties/*"); + for (Node property : propertyNodes) { + properties.putIfAbsent("${%s}".formatted(property.name()), property.textContent()); + } + current = parent(current, resolver); + } + return new Properties(properties); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private static Node parent(Node current, Function resolver) { + Node parent = current.nodeAt("/project/parent"); + if (parent != null) { + String parentGroupId = parent.textAt("groupId"); + String parentArtifactId = parent.textAt("artifactId"); + String parentVersion = parent.textAt("version"); + return resolver.apply(parentGroupId + ":" + parentArtifactId + ":" + parentVersion); + } + return null; + } + + private String replace(String input) { + if (input != null && input.startsWith("${") && input.endsWith("}")) { + String value = this.properties.get(input); + if (value != null) { + return replace(value); + } + throw new IllegalStateException("No replacement for " + input); + } + return input; + } + + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/CreateResolvedBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/CreateResolvedBom.java new file mode 100644 index 0000000000..468e5ca30e --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/CreateResolvedBom.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 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.bom; + +import java.io.FileWriter; +import java.io.IOException; + +import javax.inject.Inject; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** + * {@link Task} to create a {@link ResolvedBom resolved bom}. + * + * @author Andy Wilkinson + */ +public abstract class CreateResolvedBom extends DefaultTask { + + private final BomExtension bomExtension; + + private final BomResolver bomResolver; + + @Inject + public CreateResolvedBom(BomExtension bomExtension) { + this.bomExtension = bomExtension; + this.bomResolver = new BomResolver(getProject().getConfigurations(), getProject().getDependencies()); + getOutputFile().convention(getProject().getLayout().getBuildDirectory().file(getName() + "/resolved-bom.json")); + } + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + void describeDependencyManagement() throws IOException { + ResolvedBom dependencyManagement = this.bomResolver.resolve(this.bomExtension); + try (FileWriter writer = new FileWriter(getOutputFile().get().getAsFile())) { + dependencyManagement.writeTo(writer); + } + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/ResolvedBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/ResolvedBom.java new file mode 100644 index 0000000000..3fa12f7274 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/ResolvedBom.java @@ -0,0 +1,112 @@ +/* + * Copyright 2025 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.bom; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * A resolved bom. + * + * @author Andy Wilkinson + * @param id the ID of the resolved bom + * @param libraries the libraries declared in the bom + */ +public record ResolvedBom(Id id, List libraries) { + + private static final ObjectMapper objectMapper; + + static { + ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT) + .setDefaultPropertyInclusion(Include.NON_EMPTY); + mapper.configOverride(List.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); + objectMapper = mapper; + } + + public static ResolvedBom readFrom(File file) { + try (FileReader reader = new FileReader(file)) { + return objectMapper.readValue(reader, ResolvedBom.class); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public void writeTo(Writer writer) { + try { + objectMapper.writeValue(writer, this); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public record ResolvedLibrary(String name, String version, String versionProperty, List managedDependencies, + List importedBoms) { + + } + + public record Id(String groupId, String artifactId, String version, String classifier) implements Comparable { + + Id(String groupId, String artifactId, String version) { + this(groupId, artifactId, version, null); + } + + @Override + public int compareTo(Id o) { + int result = this.groupId.compareTo(o.groupId); + if (result != 0) { + return result; + } + result = this.artifactId.compareTo(o.artifactId); + if (result != 0) { + return result; + } + return this.version.compareTo(o.version); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(this.groupId); + builder.append(":"); + builder.append(this.artifactId); + builder.append(":"); + builder.append(this.version); + if (this.classifier != null) { + builder.append(this.classifier); + } + return builder.toString(); + } + + } + + public record Bom(Id id, Bom parent, List managedDependencies, List importedBoms) { + + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentConstrainedVersions.java b/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentConstrainedVersions.java deleted file mode 100644 index e70502f785..0000000000 --- a/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentConstrainedVersions.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2012-2024 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.constraints; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.SetProperty; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.TaskAction; - -import org.springframework.boot.build.constraints.ExtractVersionConstraints.ConstrainedVersion; - -/** - * Task for documenting a platform's constrained versions. - * - * @author Andy Wilkinson - */ -public abstract class DocumentConstrainedVersions extends DefaultTask { - - @Input - public abstract SetProperty getConstrainedVersions(); - - @OutputFile - public abstract RegularFileProperty getOutputFile(); - - @TaskAction - public void documentConstrainedVersions() throws IOException { - File outputFile = getOutputFile().get().getAsFile(); - outputFile.getParentFile().mkdirs(); - try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) { - writer.println("|==="); - writer.println("| Group ID | Artifact ID | Version"); - for (ConstrainedVersion constrainedVersion : getConstrainedVersions().get()) { - writer.println(); - writer.printf("| `%s`%n", constrainedVersion.getGroup()); - writer.printf("| `%s`%n", constrainedVersion.getArtifact()); - writer.printf("| `%s`%n", constrainedVersion.getVersion()); - } - writer.println("|==="); - } - } - -} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/docs/DocumentManagedDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/docs/DocumentManagedDependencies.java new file mode 100644 index 0000000000..74c42612f2 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/docs/DocumentManagedDependencies.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2025 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.docs; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Set; +import java.util.TreeSet; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +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.TaskAction; + +import org.springframework.boot.build.bom.ResolvedBom; +import org.springframework.boot.build.bom.ResolvedBom.Bom; +import org.springframework.boot.build.bom.ResolvedBom.Id; +import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary; + +/** + * Task for documenting {@link ResolvedBom boms'} managed dependencies. + * + * @author Andy Wilkinson + */ +public abstract class DocumentManagedDependencies extends DefaultTask { + + private FileCollection resolvedBoms; + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public FileCollection getResolvedBoms() { + return this.resolvedBoms; + } + + public void setResolvedBoms(FileCollection resolvedBoms) { + this.resolvedBoms = resolvedBoms; + } + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void documentConstrainedVersions() throws IOException { + File outputFile = getOutputFile().get().getAsFile(); + outputFile.getParentFile().mkdirs(); + try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) { + writer.println("|==="); + writer.println("| Group ID | Artifact ID | Version"); + Set managedCoordinates = new TreeSet<>((id1, id2) -> { + int result = id1.groupId().compareTo(id2.groupId()); + if (result != 0) { + return result; + } + return id1.artifactId().compareTo(id2.artifactId()); + }); + for (File file : getResolvedBoms().getFiles()) { + managedCoordinates.addAll(process(ResolvedBom.readFrom(file))); + } + for (Id id : managedCoordinates) { + writer.println(); + writer.printf("| `%s`%n", id.groupId()); + writer.printf("| `%s`%n", id.artifactId()); + writer.printf("| `%s`%n", id.version()); + } + writer.println("|==="); + } + } + + private Set process(ResolvedBom resolvedBom) { + TreeSet managedCoordinates = new TreeSet<>(); + for (ResolvedLibrary library : resolvedBom.libraries()) { + for (Id managedDependency : library.managedDependencies()) { + managedCoordinates.add(managedDependency); + } + for (Bom importedBom : library.importedBoms()) { + managedCoordinates.addAll(process(importedBom)); + } + } + return managedCoordinates; + } + + private Set process(Bom bom) { + TreeSet managedCoordinates = new TreeSet<>(); + bom.managedDependencies().stream().forEach(managedCoordinates::add); + Bom parent = bom.parent(); + if (parent != null) { + managedCoordinates.addAll(process(parent)); + } + return managedCoordinates; + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentVersionProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/docs/DocumentVersionProperties.java similarity index 54% rename from buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentVersionProperties.java rename to buildSrc/src/main/java/org/springframework/boot/build/docs/DocumentVersionProperties.java index ef1c7c8647..72bcf7716a 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentVersionProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/docs/DocumentVersionProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -14,46 +14,66 @@ * limitations under the License. */ -package org.springframework.boot.build.constraints; +package org.springframework.boot.build.docs; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.util.List; import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.SetProperty; -import org.gradle.api.tasks.Input; +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.TaskAction; -import org.springframework.boot.build.constraints.ExtractVersionConstraints.VersionProperty; +import org.springframework.boot.build.bom.ResolvedBom; +import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary; /** - * Task for documenting available version properties. + * Task for documenting {@link ResolvedBom boms'} version properties. * * @author Christoph Dreis + * @author Andy Wilkinson */ public abstract class DocumentVersionProperties extends DefaultTask { - @Input - public abstract SetProperty getVersionProperties(); + private FileCollection resolvedBoms; + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public FileCollection getResolvedBoms() { + return this.resolvedBoms; + } + + public void setResolvedBoms(FileCollection resolvedBoms) { + this.resolvedBoms = resolvedBoms; + } @OutputFile public abstract RegularFileProperty getOutputFile(); @TaskAction public void documentVersionProperties() throws IOException { + List libraries = this.resolvedBoms.getFiles() + .stream() + .map(ResolvedBom::readFrom) + .flatMap((resolvedBom) -> resolvedBom.libraries().stream()) + .sorted((l1, l2) -> l1.name().compareToIgnoreCase(l2.name())) + .toList(); File outputFile = getOutputFile().getAsFile().get(); outputFile.getParentFile().mkdirs(); try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) { writer.println("|==="); writer.println("| Library | Version Property"); - for (VersionProperty versionProperty : getVersionProperties().get()) { + for (ResolvedLibrary library : libraries) { writer.println(); - writer.printf("| `%s`%n", versionProperty.getLibraryName()); - writer.printf("| `%s`%n", versionProperty.getVersionProperty()); + writer.printf("| `%s`%n", library.name()); + writer.printf("| `%s`%n", library.versionProperty()); } writer.println("|==="); } diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 9c09b53617..0d5db463b6 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -14,6 +14,7 @@ configurations { autoConfiguration configurationProperties remoteSpringApplicationExample + resolvedBom springApplicationExample testSlices all { @@ -177,6 +178,8 @@ dependencies { remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-logging")) remoteSpringApplicationExample("org.springframework:spring-web") + resolvedBom(project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "resolvedBom")) + springApplicationExample(platform(project(":spring-boot-project:spring-boot-dependencies"))) springApplicationExample(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) @@ -254,16 +257,14 @@ task documentAutoConfigurationClasses(type: org.springframework.boot.build.autoc outputDir = layout.buildDirectory.dir("generated/docs/auto-configuration-classes/documented-auto-configuration-classes/") } -task documentDependencyVersionCoordinates(type: org.springframework.boot.build.constraints.DocumentConstrainedVersions) { - dependsOn dependencyVersions - constrainedVersions.set(providers.provider { dependencyVersions.constrainedVersions }) +task documentDependencyVersionCoordinates(type: org.springframework.boot.build.docs.DocumentManagedDependencies) { outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-coordinates.adoc") + resolvedBoms = configurations.resolvedBom } -task documentDependencyVersionProperties(type: org.springframework.boot.build.constraints.DocumentVersionProperties) { - dependsOn dependencyVersions - versionProperties.set(providers.provider { dependencyVersions.versionProperties}) +task documentDependencyVersionProperties(type: org.springframework.boot.build.docs.DocumentVersionProperties) { outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-properties.adoc") + resolvedBoms = configurations.resolvedBom } task documentConfigurationProperties(type: org.springframework.boot.build.context.properties.DocumentConfigurationProperties) {