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 7280d4a833c..9e9f09798df 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 @@ -38,6 +38,9 @@ import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaPlatformPlugin; +import org.springframework.boot.build.bom.BomExtension.LibraryHandler.AlignWithHandler.PropertyHandler; +import org.springframework.boot.build.bom.BomExtension.LibraryHandler.AlignWithHandler.VersionHandler; +import org.springframework.boot.build.bom.Library.DependencyVersionAlignment; import org.springframework.boot.build.bom.Library.Exclusion; import org.springframework.boot.build.bom.Library.Group; import org.springframework.boot.build.bom.Library.ImportedBom; @@ -45,6 +48,7 @@ import org.springframework.boot.build.bom.Library.LibraryVersion; import org.springframework.boot.build.bom.Library.Link; import org.springframework.boot.build.bom.Library.Module; import org.springframework.boot.build.bom.Library.PermittedDependency; +import org.springframework.boot.build.bom.Library.PomPropertyVersionAlignment; import org.springframework.boot.build.bom.Library.ProhibitedVersion; import org.springframework.boot.build.bom.Library.VersionAlignment; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; @@ -106,16 +110,26 @@ public class BomExtension { (version != null) ? version : ""); action.execute(libraryHandler); LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version)); - 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), libraryHandler.alignWith.dependencyManagementDeclaredIn, libraryHandler.linkRootName, libraryHandler.links)); } + private VersionAlignment versionAlignment(LibraryHandler libraryHandler) { + VersionHandler version = libraryHandler.alignWith.version; + if (version != null) { + return new DependencyVersionAlignment(version.from, version.managedBy, this.project, this.libraries, + libraryHandler.groups); + } + PropertyHandler property = libraryHandler.alignWith.property; + if (property != null) { + return new PomPropertyVersionAlignment(property.name, property.of, property.managedBy, this.project, + this.libraries); + } + return null; + } + private String createDependencyNotation(String groupId, String artifactId, DependencyVersion version) { return groupId + ":" + artifactId + ":" + version; } @@ -382,6 +396,8 @@ public class BomExtension { private VersionHandler version; + private PropertyHandler property; + private String dependencyManagementDeclaredIn; public void version(Action action) { @@ -389,6 +405,11 @@ public class BomExtension { action.execute(this.version); } + public void property(Action action) { + this.property = new PropertyHandler(); + action.execute(this.property); + } + public void dependencyManagementDeclaredIn(String bomCoordinates) { this.dependencyManagementDeclaredIn = bomCoordinates; } @@ -409,6 +430,28 @@ public class BomExtension { } + public static class PropertyHandler { + + private String name; + + private String of; + + private String managedBy; + + public void name(String name) { + this.name = name; + } + + public void of(String dependency) { + this.of = dependency; + } + + public void managedBy(String managedBy) { + this.managedBy = managedBy; + } + + } + } } 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 7c77d60485f..9c8348f25d1 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 @@ -16,6 +16,7 @@ package org.springframework.boot.build.bom; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -31,6 +32,11 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathFactory; + import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; import org.gradle.api.Project; @@ -38,6 +44,7 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.result.DependencyResult; import org.gradle.api.artifacts.result.ResolutionResult; +import org.w3c.dom.Document; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; @@ -406,10 +413,16 @@ public class Library { } + public interface VersionAlignment { + + Set resolve(); + + } + /** - * Version alignment for a library. + * Version alignment for a library based on a dependency of another module. */ - public static class VersionAlignment { + public static class DependencyVersionAlignment implements VersionAlignment { private final String from; @@ -423,7 +436,8 @@ public class Library { private Set alignedVersions; - VersionAlignment(String from, String managedBy, Project project, List libraries, List groups) { + DependencyVersionAlignment(String from, String managedBy, Project project, List libraries, + List groups) { this.from = from; this.managedBy = managedBy; this.project = project; @@ -431,6 +445,7 @@ public class Library { this.groups = groups; } + @Override public Set resolve() { if (this.alignedVersions != null) { return this.alignedVersions; @@ -539,6 +554,100 @@ public class Library { } + /** + * Version alignment for a library based on a property in the pom of another module. + */ + public static class PomPropertyVersionAlignment implements VersionAlignment { + + private final String name; + + private final String from; + + private final String managedBy; + + private final Project project; + + private final List libraries; + + private Set alignedVersions; + + PomPropertyVersionAlignment(String name, String from, String managedBy, Project project, + List libraries) { + this.name = name; + this.from = from; + this.managedBy = managedBy; + this.project = project; + this.libraries = libraries; + } + + @Override + public Set resolve() { + if (this.alignedVersions != null) { + return this.alignedVersions; + } + Configuration alignmentConfiguration = this.project.getConfigurations() + .detachedConfiguration(getAligningDependencies().toArray(new Dependency[0])); + Set files = alignmentConfiguration.resolve(); + if (files.size() != 1) { + throw new IllegalStateException( + "Expected a single file when resolving the pom of " + this.from + " but found " + files.size()); + } + File pomFile = files.iterator().next(); + return Set.of(propertyFrom(pomFile)); + } + + private List getAligningDependencies() { + Library managingLibrary = findManagingLibrary(); + List boms = getBomDependencies(managingLibrary); + List dependencies = new ArrayList<>(); + dependencies.addAll(boms); + dependencies.add(this.project.getDependencies().create(this.from + "@pom")); + return dependencies; + } + + private Library findManagingLibrary() { + if (this.managedBy == null) { + return null; + } + return this.libraries.stream() + .filter((candidate) -> this.managedBy.equals(candidate.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Managing library '" + this.managedBy + "' not found.")); + } + + private List getBomDependencies(Library manager) { + return manager.getGroups() + .stream() + .flatMap((group) -> group.getBoms() + .stream() + .map((bom) -> this.project.getDependencies() + .platform(group.getId() + ":" + bom.name() + ":" + manager.getVersion().getVersion()))) + .toList(); + } + + private String propertyFrom(File pomFile) { + try { + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = documentBuilder.parse(pomFile); + XPath xpath = XPathFactory.newInstance().newXPath(); + return xpath.evaluate("/project/properties/" + this.name + "/text()", document); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Override + public String toString() { + String result = "version from properties of " + this.from; + if (this.managedBy != null) { + result += " that is managed by " + this.managedBy; + } + return result; + } + + } + public record Link(String rootName, Function factory, List packages) { private static final Pattern PACKAGE_EXPAND = Pattern.compile("^(.*)\\[(.*)\\]$"); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e10971e5b1b..84b9bf6239e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -956,6 +956,13 @@ bom { } } library("Jedis", "5.2.0") { + alignWith { + property { + name "jedis" + of "org.springframework.data:spring-data-redis" + managedBy "Spring Data Bom" + } + } group("redis.clients") { modules = [ "jedis" @@ -1478,6 +1485,12 @@ bom { } } library("MongoDB", "5.2.1") { + alignWith { + version { + from "org.springframework.data:spring-data-mongodb" + managedBy "Spring Data Bom" + } + } group("org.mongodb") { modules = [ "bson",