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 7fa7ed2f001..57d0935c52d 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 @@ -111,15 +111,17 @@ public class BomExtension { public void library(String name, String version, Action action) { ObjectFactory objects = this.project.getObjects(); - LibraryHandler libraryHandler = objects.newInstance(LibraryHandler.class, (version != null) ? version : ""); + LibraryHandler libraryHandler = objects.newInstance(LibraryHandler.class, this.project, + (version != null) ? version : ""); action.execute(libraryHandler); LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version)); - VersionAlignment versionAlignment = (libraryHandler.alignWithVersion != null) - ? new VersionAlignment(libraryHandler.alignWithVersion.from, libraryHandler.alignWithVersion.managedBy, - this.project, this.libraries, libraryHandler.groups) + VersionAlignment versionAlignment = (libraryHandler.alignWith.version != null) + ? new VersionAlignment(libraryHandler.alignWith.version.from, + libraryHandler.alignWith.version.managedBy, this.project, this.libraries, libraryHandler.groups) : null; addLibrary(new Library(name, libraryHandler.calendarName, libraryVersion, libraryHandler.groups, - libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment)); + libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment, + libraryHandler.alignWith.dependencyManagementDeclaredIn)); } public void effectiveBomArtifact() { @@ -219,17 +221,18 @@ public class BomExtension { private final List prohibitedVersions = new ArrayList<>(); + private final AlignWithHandler alignWith; + private boolean considerSnapshots = false; private String version; private String calendarName; - private AlignWithVersionHandler alignWithVersion; - @Inject - public LibraryHandler(String version) { + public LibraryHandler(Project project, String version) { this.version = version; + this.alignWith = project.getObjects().newInstance(AlignWithHandler.class); } public void version(String version) { @@ -258,9 +261,8 @@ public class BomExtension { handler.endsWith, handler.contains, handler.reason)); } - public void alignWithVersion(Action action) { - this.alignWithVersion = new AlignWithVersionHandler(); - action.execute(this.alignWithVersion); + public void alignWith(Action action) { + action.execute(this.alignWith); } public static class ProhibitedHandler { @@ -380,18 +382,35 @@ public class BomExtension { } - public static class AlignWithVersionHandler { + public static class AlignWithHandler { - private String from; + private VersionHandler version; - private String managedBy; + private String dependencyManagementDeclaredIn; - public void from(String from) { - this.from = from; + public void version(Action action) { + this.version = new VersionHandler(); + action.execute(this.version); } - public void managedBy(String managedBy) { - this.managedBy = managedBy; + public void dependencyManagementDeclaredIn(String bomCoordinates) { + this.dependencyManagementDeclaredIn = bomCoordinates; + } + + public static class VersionHandler { + + private String from; + + private String managedBy; + + public void from(String from) { + this.from = from; + } + + public void managedBy(String managedBy) { + this.managedBy = managedBy; + } + } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java index 3bd688c59ec..f600097b3d5 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java @@ -16,6 +16,7 @@ package org.springframework.boot.build.bom; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -36,6 +37,7 @@ import org.springframework.boot.build.bom.Library.Group; import org.springframework.boot.build.bom.Library.Module; import org.springframework.boot.build.bom.Library.ProhibitedVersion; import org.springframework.boot.build.bom.Library.VersionAlignment; +import org.springframework.boot.build.bom.ManagedDependencies.Difference; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; /** @@ -71,6 +73,7 @@ public class CheckBom extends DefaultTask { checkExclusions(library, libraryErrors); checkProhibitedVersions(library, libraryErrors); checkVersionAlignment(library, libraryErrors); + checkDependencyManagementAlignment(library, libraryErrors); if (!libraryErrors.isEmpty()) { errors.add(library.getName()); for (String libraryError : libraryErrors) { @@ -174,4 +177,40 @@ public class CheckBom extends DefaultTask { } } + private void checkDependencyManagementAlignment(Library library, List errors) { + String alignsWithBom = library.getAlignsWithBom(); + if (alignsWithBom == null) { + return; + } + File bom = resolveBom(library, alignsWithBom); + ManagedDependencies managedByBom = ManagedDependencies.ofBom(bom); + ManagedDependencies managedByLibrary = ManagedDependencies.ofLibrary(library); + Difference diff = managedByBom.diff(managedByLibrary); + if (!diff.isEmpty()) { + String error = "Dependency management does not align with " + library.getAlignsWithBom() + ":"; + if (!diff.missing().isEmpty()) { + error = error + "%n - Missing:%n %s" + .formatted(String.join("\n ", diff.missing())); + } + if (!diff.unexpected().isEmpty()) { + error = error + "%n - Unexpected:%n %s" + .formatted(String.join("\n ", diff.unexpected())); + } + errors.add(error); + } + } + + private File resolveBom(Library library, String alignsWithBom) { + String coordinates = alignsWithBom + ":" + library.getVersion().getVersion() + "@pom"; + Set files = getProject().getConfigurations() + .detachedConfiguration(getProject().getDependencies().create(coordinates)) + .getResolvedConfiguration() + .getFiles(); + if (files.size() != 1) { + throw new IllegalStateException( + "Expected a single file but '" + coordinates + "' resolved to " + files.size()); + } + return files.iterator().next(); + } + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java index 8fa5f37f2f6..b1f66f2b1d1 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java @@ -59,6 +59,8 @@ public class Library { private final VersionAlignment versionAlignment; + private final String alignsWithBom; + /** * Create a new {@code Library} with the given {@code name}, {@code version}, and * {@code groups}. @@ -70,9 +72,12 @@ public class Library { * @param prohibitedVersions version of the library that are prohibited * @param considerSnapshots whether to consider snapshots * @param versionAlignment version alignment, if any, for the library + * @param alignsWithBom the coordinates of the bom, if any, that this library should + * inline */ public Library(String name, String calendarName, LibraryVersion version, List groups, - List prohibitedVersions, boolean considerSnapshots, VersionAlignment versionAlignment) { + List prohibitedVersions, boolean considerSnapshots, VersionAlignment versionAlignment, + String alignsWithBom) { this.name = name; this.calendarName = (calendarName != null) ? calendarName : name; this.version = version; @@ -82,6 +87,7 @@ public class Library { this.prohibitedVersions = prohibitedVersions; this.considerSnapshots = considerSnapshots; this.versionAlignment = versionAlignment; + this.alignsWithBom = alignsWithBom; } public String getName() { @@ -116,6 +122,10 @@ public class Library { return this.versionAlignment; } + public String getAlignsWithBom() { + return this.alignsWithBom; + } + /** * A version or range of versions that are prohibited from being used in a bom. */ diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/ManagedDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/ManagedDependencies.java new file mode 100644 index 00000000000..c9066aa053b --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/ManagedDependencies.java @@ -0,0 +1,127 @@ +/* + * 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.bom; + +import java.io.File; +import java.io.FileReader; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import org.springframework.boot.build.bom.Library.Group; +import org.springframework.boot.build.bom.Library.Module; + +/** + * Managed dependencies from a bom or library. + * + * @author Andy Wilkinson + */ +class ManagedDependencies { + + private final Set ids; + + ManagedDependencies(Set ids) { + this.ids = ids; + } + + Set getIds() { + return this.ids; + } + + Difference diff(ManagedDependencies other) { + Set missing = new HashSet<>(this.ids); + missing.removeAll(other.ids); + Set unexpected = new HashSet<>(other.ids); + unexpected.removeAll(this.ids); + return new Difference(missing, unexpected); + } + + static ManagedDependencies ofBom(File bom) { + try { + Document bomDocument = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(new InputSource(new FileReader(bom))); + XPath xpath = XPathFactory.newInstance().newXPath(); + NodeList dependencyNodes = (NodeList) xpath + .evaluate("/project/dependencyManagement/dependencies/dependency", bomDocument, XPathConstants.NODESET); + NodeList propertyNodes = (NodeList) xpath.evaluate("/project/properties/*", bomDocument, + XPathConstants.NODESET); + Map properties = new HashMap<>(); + for (int i = 0; i < propertyNodes.getLength(); i++) { + Node property = propertyNodes.item(i); + String name = property.getNodeName(); + String value = property.getTextContent(); + properties.put("${%s}".formatted(name), value); + } + Set managedDependencies = new HashSet<>(); + for (int i = 0; i < dependencyNodes.getLength(); i++) { + Node dependency = dependencyNodes.item(i); + String groupId = (String) xpath.evaluate("groupId/text()", dependency, XPathConstants.STRING); + String artifactId = (String) xpath.evaluate("artifactId/text()", dependency, XPathConstants.STRING); + String version = (String) xpath.evaluate("version/text()", dependency, XPathConstants.STRING); + String classifier = (String) xpath.evaluate("classifier/text()", dependency, XPathConstants.STRING); + if (version.startsWith("${") && version.endsWith("}")) { + version = properties.get(version); + } + managedDependencies.add(asId(groupId, artifactId, version, classifier)); + } + return new ManagedDependencies(managedDependencies); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + static String asId(String groupId, String artifactId, String version, String classifier) { + String id = groupId + ":" + artifactId + ":" + version; + if (classifier != null && classifier.length() > 0) { + id = id + ":" + classifier; + } + return id; + } + + static ManagedDependencies ofLibrary(Library library) { + Set managedByLibrary = new HashSet<>(); + for (Group group : library.getGroups()) { + for (Module module : group.getModules()) { + managedByLibrary.add(asId(group.getId(), module.getName(), library.getVersion().getVersion().toString(), + module.getClassifier())); + } + } + return new ManagedDependencies(managedByLibrary); + } + + record Difference(Set missing, Set unexpected) { + + boolean isEmpty() { + return this.missing.isEmpty() && this.unexpected.isEmpty(); + } + + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java index 7ef6061fb11..a074ca858ae 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java @@ -53,7 +53,7 @@ class UpgradeApplicatorTests { FileCopyUtils.copy(new File("src/test/resources/gradle.properties"), gradleProperties); new UpgradeApplicator(bom.toPath(), gradleProperties.toPath()) .apply(new Upgrade(new Library("ActiveMQ", null, new LibraryVersion(DependencyVersion.parse("5.15.11")), - null, null, false, null), DependencyVersion.parse("5.16"))); + null, null, false, null, null), DependencyVersion.parse("5.16"))); String bomContents = Files.readString(bom.toPath()); assertThat(bomContents).hasSize(originalContents.length() - 3); } @@ -66,7 +66,7 @@ class UpgradeApplicatorTests { FileCopyUtils.copy(new File("src/test/resources/gradle.properties"), gradleProperties); new UpgradeApplicator(bom.toPath(), gradleProperties.toPath()) .apply(new Upgrade(new Library("Kotlin", null, new LibraryVersion(DependencyVersion.parse("1.3.70")), null, - null, false, null), DependencyVersion.parse("1.4"))); + null, false, null, null), DependencyVersion.parse("1.4"))); Properties properties = new Properties(); try (InputStream in = new FileInputStream(gradleProperties)) { properties.load(in); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 906da8a6496..0ca04ed2191 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1051,9 +1051,11 @@ bom { } } library("Neo4j Java Driver", "5.19.0") { - alignWithVersion { - from "org.springframework.data:spring-data-neo4j" - managedBy "Spring Data Bom" + alignWith { + version { + from "org.springframework.data:spring-data-neo4j" + managedBy "Spring Data Bom" + } } group("org.neo4j.driver") { modules = [