Skip to content

Commit 13bff93

Browse files
committed
Merge branch '3.4.x'
Closes gh-44880
2 parents 95b882c + 0a4f6b1 commit 13bff93

File tree

9 files changed

+631
-80
lines changed

9 files changed

+631
-80
lines changed

buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java

+7
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
*/
8383
public class BomExtension {
8484

85+
private final String id;
86+
8587
private final Project project;
8688

8789
private final UpgradeHandler upgradeHandler;
@@ -95,6 +97,11 @@ public class BomExtension {
9597
public BomExtension(Project project) {
9698
this.project = project;
9799
this.upgradeHandler = project.getObjects().newInstance(UpgradeHandler.class, project);
100+
this.id = "%s:%s:%s".formatted(project.getGroup(), project.getName(), project.getVersion());
101+
}
102+
103+
public String getId() {
104+
return this.id;
98105
}
99106

100107
public List<Library> getLibraries() {

buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java

+6
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,17 @@ public void apply(Project project) {
6262
javaPlatform.allowDependencies();
6363
createApiEnforcedConfiguration(project);
6464
BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project);
65+
TaskProvider<CreateResolvedBom> createResolvedBom = project.getTasks()
66+
.register("createResolvedBom", CreateResolvedBom.class, bom);
6567
TaskProvider<CheckBom> checkBom = project.getTasks().register("bomrCheck", CheckBom.class, bom);
6668
project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom));
6769
project.getTasks().register("bomrUpgrade", UpgradeBom.class, bom);
6870
project.getTasks().register("moveToSnapshots", MoveToSnapshots.class, bom);
6971
project.getTasks().register("checkLinks", CheckLinks.class, bom);
72+
Configuration resolvedBomConfiguration = project.getConfigurations().create("resolvedBom");
73+
project.getArtifacts()
74+
.add(resolvedBomConfiguration.getName(), createResolvedBom.map(CreateResolvedBom::getOutputFile),
75+
(artifact) -> artifact.builtBy(createResolvedBom));
7076
new PublishingCustomizer(project, bom).customize();
7177
}
7278

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.build.bom;
18+
19+
import java.io.File;
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import java.util.function.Function;
26+
27+
import javax.xml.namespace.QName;
28+
import javax.xml.parsers.DocumentBuilder;
29+
import javax.xml.parsers.DocumentBuilderFactory;
30+
import javax.xml.parsers.ParserConfigurationException;
31+
import javax.xml.xpath.XPath;
32+
import javax.xml.xpath.XPathConstants;
33+
import javax.xml.xpath.XPathExpressionException;
34+
import javax.xml.xpath.XPathFactory;
35+
36+
import org.gradle.api.artifacts.ConfigurationContainer;
37+
import org.gradle.api.artifacts.ResolvedArtifact;
38+
import org.gradle.api.artifacts.dsl.DependencyHandler;
39+
import org.w3c.dom.Document;
40+
import org.w3c.dom.NodeList;
41+
42+
import org.springframework.boot.build.bom.Library.Group;
43+
import org.springframework.boot.build.bom.Library.Module;
44+
import org.springframework.boot.build.bom.ResolvedBom.Bom;
45+
import org.springframework.boot.build.bom.ResolvedBom.Id;
46+
import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary;
47+
48+
/**
49+
* Creates a {@link ResolvedBom resolved bom}.
50+
*
51+
* @author Andy Wilkinson
52+
*/
53+
class BomResolver {
54+
55+
private final ConfigurationContainer configurations;
56+
57+
private final DependencyHandler dependencies;
58+
59+
private final DocumentBuilder documentBuilder;
60+
61+
BomResolver(ConfigurationContainer configurations, DependencyHandler dependencies) {
62+
this.configurations = configurations;
63+
this.dependencies = dependencies;
64+
try {
65+
this.documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
66+
}
67+
catch (ParserConfigurationException ex) {
68+
throw new RuntimeException(ex);
69+
}
70+
}
71+
72+
ResolvedBom resolve(BomExtension bomExtension) {
73+
List<ResolvedLibrary> libraries = new ArrayList<>();
74+
for (Library library : bomExtension.getLibraries()) {
75+
List<Id> managedDependencies = new ArrayList<>();
76+
List<Bom> imports = new ArrayList<>();
77+
for (Group group : library.getGroups()) {
78+
for (Module module : group.getModules()) {
79+
Id id = new Id(group.getId(), module.getName(), library.getVersion().getVersion().toString());
80+
managedDependencies.add(id);
81+
}
82+
for (String imported : group.getBoms()) {
83+
Bom bom = bomFrom(resolveBom(
84+
"%s:%s:%s".formatted(group.getId(), imported, library.getVersion().getVersion())));
85+
imports.add(bom);
86+
}
87+
}
88+
ResolvedLibrary resolvedLibrary = new ResolvedLibrary(library.getName(),
89+
library.getVersion().getVersion().toString(), library.getVersionProperty(), managedDependencies,
90+
imports);
91+
libraries.add(resolvedLibrary);
92+
}
93+
String[] idComponents = bomExtension.getId().split(":");
94+
return new ResolvedBom(new Id(idComponents[0], idComponents[1], idComponents[2]), libraries);
95+
}
96+
97+
Bom resolveMavenBom(String coordinates) {
98+
return bomFrom(resolveBom(coordinates));
99+
}
100+
101+
private File resolveBom(String coordinates) {
102+
Set<ResolvedArtifact> artifacts = this.configurations
103+
.detachedConfiguration(this.dependencies.create(coordinates + "@pom"))
104+
.getResolvedConfiguration()
105+
.getResolvedArtifacts();
106+
if (artifacts.size() != 1) {
107+
throw new IllegalStateException("Expected a single artifact but '%s' resolved to %d artifacts"
108+
.formatted(coordinates, artifacts.size()));
109+
}
110+
return artifacts.iterator().next().getFile();
111+
}
112+
113+
private Bom bomFrom(File bomFile) {
114+
try {
115+
Node bom = nodeFrom(bomFile);
116+
File parentBomFile = parentBomFile(bom);
117+
Bom parent = null;
118+
if (parentBomFile != null) {
119+
parent = bomFrom(parentBomFile);
120+
}
121+
Properties properties = Properties.from(bom, this::nodeFrom);
122+
List<Node> dependencyNodes = bom.nodesAt("/project/dependencyManagement/dependencies/dependency");
123+
List<Id> managedDependencies = new ArrayList<>();
124+
List<Bom> imports = new ArrayList<>();
125+
for (Node dependency : dependencyNodes) {
126+
String groupId = properties.replace(dependency.textAt("groupId"));
127+
String artifactId = properties.replace(dependency.textAt("artifactId"));
128+
String version = properties.replace(dependency.textAt("version"));
129+
String classifier = properties.replace(dependency.textAt("classifier"));
130+
String scope = properties.replace(dependency.textAt("scope"));
131+
Bom importedBom = null;
132+
if ("import".equals(scope)) {
133+
String type = properties.replace(dependency.textAt("type"));
134+
if ("pom".equals(type)) {
135+
importedBom = bomFrom(resolveBom(groupId + ":" + artifactId + ":" + version));
136+
}
137+
}
138+
if (importedBom != null) {
139+
imports.add(importedBom);
140+
}
141+
else {
142+
managedDependencies.add(new Id(groupId, artifactId, version, classifier));
143+
}
144+
}
145+
String groupId = bom.textAt("/project/groupId");
146+
if ((groupId == null || groupId.isEmpty()) && parent != null) {
147+
groupId = parent.id().groupId();
148+
}
149+
String artifactId = bom.textAt("/project/artifactId");
150+
String version = bom.textAt("/project/version");
151+
if ((version == null || version.isEmpty()) && parent != null) {
152+
version = parent.id().version();
153+
}
154+
return new Bom(new Id(groupId, artifactId, version), parent, managedDependencies, imports);
155+
}
156+
catch (Exception ex) {
157+
throw new RuntimeException(ex);
158+
}
159+
}
160+
161+
private Node nodeFrom(String coordinates) {
162+
return nodeFrom(resolveBom(coordinates));
163+
}
164+
165+
private Node nodeFrom(File bomFile) {
166+
try {
167+
Document document = this.documentBuilder.parse(bomFile);
168+
return new Node(document);
169+
}
170+
catch (Exception ex) {
171+
throw new RuntimeException(ex);
172+
}
173+
}
174+
175+
private File parentBomFile(Node bom) {
176+
Node parent = bom.nodeAt("/project/parent");
177+
if (parent != null) {
178+
String parentGroupId = parent.textAt("groupId");
179+
String parentArtifactId = parent.textAt("artifactId");
180+
String parentVersion = parent.textAt("version");
181+
return resolveBom(parentGroupId + ":" + parentArtifactId + ":" + parentVersion);
182+
}
183+
return null;
184+
}
185+
186+
private static final class Node {
187+
188+
protected final XPath xpath;
189+
190+
private final org.w3c.dom.Node delegate;
191+
192+
private Node(org.w3c.dom.Node delegate) {
193+
this(delegate, XPathFactory.newInstance().newXPath());
194+
}
195+
196+
private Node(org.w3c.dom.Node delegate, XPath xpath) {
197+
this.delegate = delegate;
198+
this.xpath = xpath;
199+
}
200+
201+
private String textAt(String expression) {
202+
String text = (String) evaluate(expression + "/text()", XPathConstants.STRING);
203+
return (text != null && !text.isBlank()) ? text : null;
204+
}
205+
206+
private Node nodeAt(String expression) {
207+
org.w3c.dom.Node result = (org.w3c.dom.Node) evaluate(expression, XPathConstants.NODE);
208+
return (result != null) ? new Node(result, this.xpath) : null;
209+
}
210+
211+
private List<Node> nodesAt(String expression) {
212+
NodeList nodes = (NodeList) evaluate(expression, XPathConstants.NODESET);
213+
List<Node> things = new ArrayList<>(nodes.getLength());
214+
for (int i = 0; i < nodes.getLength(); i++) {
215+
things.add(new Node(nodes.item(i), this.xpath));
216+
}
217+
return things;
218+
}
219+
220+
private Object evaluate(String expression, QName type) {
221+
try {
222+
return this.xpath.evaluate(expression, this.delegate, type);
223+
}
224+
catch (XPathExpressionException ex) {
225+
throw new RuntimeException(ex);
226+
}
227+
}
228+
229+
private String name() {
230+
return this.delegate.getNodeName();
231+
}
232+
233+
private String textContent() {
234+
return this.delegate.getTextContent();
235+
}
236+
237+
}
238+
239+
private static final class Properties {
240+
241+
private final Map<String, String> properties;
242+
243+
private Properties(Map<String, String> properties) {
244+
this.properties = properties;
245+
}
246+
247+
private static Properties from(Node bom, Function<String, Node> resolver) {
248+
try {
249+
Map<String, String> properties = new HashMap<>();
250+
Node current = bom;
251+
while (current != null) {
252+
String groupId = current.textAt("/project/groupId");
253+
if (groupId != null && !groupId.isEmpty()) {
254+
properties.putIfAbsent("${project.groupId}", groupId);
255+
}
256+
String version = current.textAt("/project/version");
257+
if (version != null && !version.isEmpty()) {
258+
properties.putIfAbsent("${project.version}", version);
259+
}
260+
List<Node> propertyNodes = current.nodesAt("/project/properties/*");
261+
for (Node property : propertyNodes) {
262+
properties.putIfAbsent("${%s}".formatted(property.name()), property.textContent());
263+
}
264+
current = parent(current, resolver);
265+
}
266+
return new Properties(properties);
267+
}
268+
catch (Exception ex) {
269+
throw new RuntimeException(ex);
270+
}
271+
}
272+
273+
private static Node parent(Node current, Function<String, Node> resolver) {
274+
Node parent = current.nodeAt("/project/parent");
275+
if (parent != null) {
276+
String parentGroupId = parent.textAt("groupId");
277+
String parentArtifactId = parent.textAt("artifactId");
278+
String parentVersion = parent.textAt("version");
279+
return resolver.apply(parentGroupId + ":" + parentArtifactId + ":" + parentVersion);
280+
}
281+
return null;
282+
}
283+
284+
private String replace(String input) {
285+
if (input != null && input.startsWith("${") && input.endsWith("}")) {
286+
String value = this.properties.get(input);
287+
if (value != null) {
288+
return replace(value);
289+
}
290+
throw new IllegalStateException("No replacement for " + input);
291+
}
292+
return input;
293+
}
294+
295+
}
296+
297+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.build.bom;
18+
19+
import java.io.FileWriter;
20+
import java.io.IOException;
21+
22+
import javax.inject.Inject;
23+
24+
import org.gradle.api.DefaultTask;
25+
import org.gradle.api.Task;
26+
import org.gradle.api.file.RegularFileProperty;
27+
import org.gradle.api.tasks.OutputFile;
28+
import org.gradle.api.tasks.TaskAction;
29+
30+
/**
31+
* {@link Task} to create a {@link ResolvedBom resolved bom}.
32+
*
33+
* @author Andy Wilkinson
34+
*/
35+
public abstract class CreateResolvedBom extends DefaultTask {
36+
37+
private final BomExtension bomExtension;
38+
39+
private final BomResolver bomResolver;
40+
41+
@Inject
42+
public CreateResolvedBom(BomExtension bomExtension) {
43+
this.bomExtension = bomExtension;
44+
this.bomResolver = new BomResolver(getProject().getConfigurations(), getProject().getDependencies());
45+
getOutputFile().convention(getProject().getLayout().getBuildDirectory().file(getName() + "/resolved-bom.json"));
46+
}
47+
48+
@OutputFile
49+
public abstract RegularFileProperty getOutputFile();
50+
51+
@TaskAction
52+
void describeDependencyManagement() throws IOException {
53+
ResolvedBom dependencyManagement = this.bomResolver.resolve(this.bomExtension);
54+
try (FileWriter writer = new FileWriter(getOutputFile().get().getAsFile())) {
55+
dependencyManagement.writeTo(writer);
56+
}
57+
}
58+
59+
}

0 commit comments

Comments
 (0)