|
| 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 | +} |
0 commit comments