commit
0a4f6b14ef
|
@ -82,6 +82,8 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||||
*/
|
*/
|
||||||
public class BomExtension {
|
public class BomExtension {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
private final Project project;
|
private final Project project;
|
||||||
|
|
||||||
private final UpgradeHandler upgradeHandler;
|
private final UpgradeHandler upgradeHandler;
|
||||||
|
@ -95,6 +97,11 @@ public class BomExtension {
|
||||||
public BomExtension(Project project) {
|
public BomExtension(Project project) {
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.upgradeHandler = project.getObjects().newInstance(UpgradeHandler.class, 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<Library> getLibraries() {
|
public List<Library> getLibraries() {
|
||||||
|
|
|
@ -62,11 +62,17 @@ public class BomPlugin implements Plugin<Project> {
|
||||||
javaPlatform.allowDependencies();
|
javaPlatform.allowDependencies();
|
||||||
createApiEnforcedConfiguration(project);
|
createApiEnforcedConfiguration(project);
|
||||||
BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project);
|
BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project);
|
||||||
|
TaskProvider<CreateResolvedBom> createResolvedBom = project.getTasks()
|
||||||
|
.register("createResolvedBom", CreateResolvedBom.class, bom);
|
||||||
TaskProvider<CheckBom> checkBom = project.getTasks().register("bomrCheck", CheckBom.class, bom);
|
TaskProvider<CheckBom> checkBom = project.getTasks().register("bomrCheck", CheckBom.class, bom);
|
||||||
project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom));
|
project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom));
|
||||||
project.getTasks().register("bomrUpgrade", UpgradeBom.class, bom);
|
project.getTasks().register("bomrUpgrade", UpgradeBom.class, bom);
|
||||||
project.getTasks().register("moveToSnapshots", MoveToSnapshots.class, bom);
|
project.getTasks().register("moveToSnapshots", MoveToSnapshots.class, bom);
|
||||||
project.getTasks().register("checkLinks", CheckLinks.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();
|
new PublishingCustomizer(project, bom).customize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<ResolvedLibrary> libraries = new ArrayList<>();
|
||||||
|
for (Library library : bomExtension.getLibraries()) {
|
||||||
|
List<Id> managedDependencies = new ArrayList<>();
|
||||||
|
List<Bom> 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<ResolvedArtifact> 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<Node> dependencyNodes = bom.nodesAt("/project/dependencyManagement/dependencies/dependency");
|
||||||
|
List<Id> managedDependencies = new ArrayList<>();
|
||||||
|
List<Bom> 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<Node> nodesAt(String expression) {
|
||||||
|
NodeList nodes = (NodeList) evaluate(expression, XPathConstants.NODESET);
|
||||||
|
List<Node> 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<String, String> properties;
|
||||||
|
|
||||||
|
private Properties(Map<String, String> properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Properties from(Node bom, Function<String, Node> resolver) {
|
||||||
|
try {
|
||||||
|
Map<String, String> 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<Node> 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<String, Node> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<ResolvedLibrary> 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<Id> managedDependencies,
|
||||||
|
List<Bom> importedBoms) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Id(String groupId, String artifactId, String version, String classifier) implements Comparable<Id> {
|
||||||
|
|
||||||
|
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<Id> managedDependencies, List<Bom> importedBoms) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<ConstrainedVersion> 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("|===");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<Id> 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<Id> process(ResolvedBom resolvedBom) {
|
||||||
|
TreeSet<Id> 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<Id> process(Bom bom) {
|
||||||
|
TreeSet<Id> managedCoordinates = new TreeSet<>();
|
||||||
|
bom.managedDependencies().stream().forEach(managedCoordinates::add);
|
||||||
|
Bom parent = bom.parent();
|
||||||
|
if (parent != null) {
|
||||||
|
managedCoordinates.addAll(process(parent));
|
||||||
|
}
|
||||||
|
return managedCoordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,46 +14,66 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.build.constraints;
|
package org.springframework.boot.build.docs;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.gradle.api.DefaultTask;
|
import org.gradle.api.DefaultTask;
|
||||||
|
import org.gradle.api.file.FileCollection;
|
||||||
import org.gradle.api.file.RegularFileProperty;
|
import org.gradle.api.file.RegularFileProperty;
|
||||||
import org.gradle.api.provider.SetProperty;
|
import org.gradle.api.tasks.InputFiles;
|
||||||
import org.gradle.api.tasks.Input;
|
|
||||||
import org.gradle.api.tasks.OutputFile;
|
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.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 Christoph Dreis
|
||||||
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
public abstract class DocumentVersionProperties extends DefaultTask {
|
public abstract class DocumentVersionProperties extends DefaultTask {
|
||||||
|
|
||||||
@Input
|
private FileCollection resolvedBoms;
|
||||||
public abstract SetProperty<VersionProperty> getVersionProperties();
|
|
||||||
|
@InputFiles
|
||||||
|
@PathSensitive(PathSensitivity.RELATIVE)
|
||||||
|
public FileCollection getResolvedBoms() {
|
||||||
|
return this.resolvedBoms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResolvedBoms(FileCollection resolvedBoms) {
|
||||||
|
this.resolvedBoms = resolvedBoms;
|
||||||
|
}
|
||||||
|
|
||||||
@OutputFile
|
@OutputFile
|
||||||
public abstract RegularFileProperty getOutputFile();
|
public abstract RegularFileProperty getOutputFile();
|
||||||
|
|
||||||
@TaskAction
|
@TaskAction
|
||||||
public void documentVersionProperties() throws IOException {
|
public void documentVersionProperties() throws IOException {
|
||||||
|
List<ResolvedLibrary> 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();
|
File outputFile = getOutputFile().getAsFile().get();
|
||||||
outputFile.getParentFile().mkdirs();
|
outputFile.getParentFile().mkdirs();
|
||||||
try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
|
try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
|
||||||
writer.println("|===");
|
writer.println("|===");
|
||||||
writer.println("| Library | Version Property");
|
writer.println("| Library | Version Property");
|
||||||
for (VersionProperty versionProperty : getVersionProperties().get()) {
|
for (ResolvedLibrary library : libraries) {
|
||||||
writer.println();
|
writer.println();
|
||||||
writer.printf("| `%s`%n", versionProperty.getLibraryName());
|
writer.printf("| `%s`%n", library.name());
|
||||||
writer.printf("| `%s`%n", versionProperty.getVersionProperty());
|
writer.printf("| `%s`%n", library.versionProperty());
|
||||||
}
|
}
|
||||||
writer.println("|===");
|
writer.println("|===");
|
||||||
}
|
}
|
|
@ -14,6 +14,7 @@ configurations {
|
||||||
autoConfiguration
|
autoConfiguration
|
||||||
configurationProperties
|
configurationProperties
|
||||||
remoteSpringApplicationExample
|
remoteSpringApplicationExample
|
||||||
|
resolvedBom
|
||||||
springApplicationExample
|
springApplicationExample
|
||||||
testSlices
|
testSlices
|
||||||
all {
|
all {
|
||||||
|
@ -178,6 +179,8 @@ dependencies {
|
||||||
remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-logging"))
|
remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-logging"))
|
||||||
remoteSpringApplicationExample("org.springframework:spring-web")
|
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(platform(project(":spring-boot-project:spring-boot-dependencies")))
|
||||||
springApplicationExample(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
|
springApplicationExample(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
|
||||||
|
|
||||||
|
@ -255,16 +258,14 @@ task documentAutoConfigurationClasses(type: org.springframework.boot.build.autoc
|
||||||
outputDir = layout.buildDirectory.dir("generated/docs/auto-configuration-classes/documented-auto-configuration-classes/")
|
outputDir = layout.buildDirectory.dir("generated/docs/auto-configuration-classes/documented-auto-configuration-classes/")
|
||||||
}
|
}
|
||||||
|
|
||||||
task documentDependencyVersionCoordinates(type: org.springframework.boot.build.constraints.DocumentConstrainedVersions) {
|
task documentDependencyVersionCoordinates(type: org.springframework.boot.build.docs.DocumentManagedDependencies) {
|
||||||
dependsOn dependencyVersions
|
|
||||||
constrainedVersions.set(providers.provider { dependencyVersions.constrainedVersions })
|
|
||||||
outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-coordinates.adoc")
|
outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-coordinates.adoc")
|
||||||
|
resolvedBoms = configurations.resolvedBom
|
||||||
}
|
}
|
||||||
|
|
||||||
task documentDependencyVersionProperties(type: org.springframework.boot.build.constraints.DocumentVersionProperties) {
|
task documentDependencyVersionProperties(type: org.springframework.boot.build.docs.DocumentVersionProperties) {
|
||||||
dependsOn dependencyVersions
|
|
||||||
versionProperties.set(providers.provider { dependencyVersions.versionProperties})
|
|
||||||
outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-properties.adoc")
|
outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-properties.adoc")
|
||||||
|
resolvedBoms = configurations.resolvedBom
|
||||||
}
|
}
|
||||||
|
|
||||||
task documentConfigurationProperties(type: org.springframework.boot.build.context.properties.DocumentConfigurationProperties) {
|
task documentConfigurationProperties(type: org.springframework.boot.build.context.properties.DocumentConfigurationProperties) {
|
||||||
|
|
Loading…
Reference in New Issue