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 7280d4a833..63af79042e 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.of, 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,16 +405,27 @@ 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; } public static class VersionHandler { + private String of; + private String from; private String managedBy; + public void of(String of) { + this.of = of; + } + public void from(String from) { this.from = from; } @@ -409,6 +436,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 7c77d60485..ccc9b6683f 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,18 @@ 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 dependency; private final String from; @@ -423,7 +438,9 @@ public class Library { private Set alignedVersions; - VersionAlignment(String from, String managedBy, Project project, List libraries, List groups) { + DependencyVersionAlignment(String dependency, String from, String managedBy, Project project, + List libraries, List groups) { + this.dependency = dependency; this.from = from; this.managedBy = managedBy; this.project = project; @@ -431,27 +448,34 @@ public class Library { this.groups = groups; } + @Override public Set resolve() { if (this.alignedVersions != null) { return this.alignedVersions; } Map versions = resolveAligningDependencies(); - Set versionsInLibrary = new HashSet<>(); - for (Group group : this.groups) { - for (Module module : group.getModules()) { - String version = versions.get(group.getId() + ":" + module.getName()); - if (version != null) { - versionsInLibrary.add(version); - } - } - for (String plugin : group.getPlugins()) { - String version = versions.get(group.getId() + ":" + plugin); - if (version != null) { - versionsInLibrary.add(version); - } - } + if (this.dependency != null) { + String version = versions.get(this.dependency); + this.alignedVersions = (version != null) ? Set.of(version) : Collections.emptySet(); + } + else { + Set versionsInLibrary = new HashSet<>(); + for (Group group : this.groups) { + for (Module module : group.getModules()) { + String version = versions.get(group.getId() + ":" + module.getName()); + if (version != null) { + versionsInLibrary.add(version); + } + } + for (String plugin : group.getPlugins()) { + String version = versions.get(group.getId() + ":" + plugin); + if (version != null) { + versionsInLibrary.add(version); + } + } + } + this.alignedVersions = versionsInLibrary; } - this.alignedVersions = versionsInLibrary; return this.alignedVersions; } @@ -539,6 +563,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 b950f0fbe4..c28e338e7b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1009,9 +1009,12 @@ bom { } } library("Jedis", "6.0.0") { - prohibit { - contains "-beta" - because "we don't want beta dependencies" + alignWith { + property { + name "jedis" + of "org.springframework.data:spring-data-redis" + managedBy "Spring Data Bom" + } } group("redis.clients") { modules = [ @@ -1596,6 +1599,13 @@ bom { } } library("MongoDB", "5.5.0") { + alignWith { + version { + of "org.mongodb:mongodb-driver-core" + from "org.springframework.data:spring-data-mongodb" + managedBy "Spring Data Bom" + } + } group("org.mongodb") { bom("mongodb-driver-bom") }