diff --git a/src/main/java/rife/bld/BaseProject.java b/src/main/java/rife/bld/BaseProject.java
index e6a8951..9e3330f 100644
--- a/src/main/java/rife/bld/BaseProject.java
+++ b/src/main/java/rife/bld/BaseProject.java
@@ -992,6 +992,61 @@ public class BaseProject extends BuildExecutor {
return Module.parse(description);
}
+ /**
+ * Creates a new bill of materials instance.
+ *
+ * @param groupId the BOM group identifier
+ * @param artifactId the BOM artifact identifier
+ * @return a newly created {@code Bom} instance
+ * @since 2.4.0
+ */
+ public Bom bom(String groupId, String artifactId) {
+ return new Bom(groupId, artifactId);
+ }
+
+ /**
+ * Creates a new bill of materials instance.
+ *
+ * @param groupId the BOM group identifier
+ * @param artifactId the BOM artifact identifier
+ * @param version the BOM version
+ * @return a newly created {@code Bom} instance
+ * @since 2.4.0
+ */
+ public Bom bom(String groupId, String artifactId, String version) {
+ return new Bom(groupId, artifactId, version(version));
+ }
+
+ /**
+ * Creates a new bill of materials instance.
+ *
+ * @param groupId the BOM group identifier
+ * @param artifactId the BOM artifact identifier
+ * @param version the BOM version
+ * @return a newly created {@code Bom} instance
+ * @since 2.4.0
+ */
+ public Bom bom(String groupId, String artifactId, Version version) {
+ return new Bom(groupId, artifactId, version);
+ }
+
+ /**
+ * Creates a new bill of materials instance from a string representation.
+ * The format is {@code groupId:artifactId:version:classifier}.
+ * The {@code version} and {@code classifier} are optional, and an
+ * optional {@code @bom} or {@code @pom} type suffix is accepted.
+ *
+ * If the string can't be successfully parsed, {@code null} will be returned.
+ *
+ * @param description the BOM string to parse
+ * @return a parsed instance of {@code Bom}; or
+ * {@code null} when the string couldn't be parsed
+ * @since 2.4.0
+ */
+ public Bom bom(String description) {
+ return Bom.parse(description);
+ }
+
/**
* Creates a local module instance.
*
diff --git a/src/main/java/rife/bld/BldCache.java b/src/main/java/rife/bld/BldCache.java
index ee90a26..282beeb 100644
--- a/src/main/java/rife/bld/BldCache.java
+++ b/src/main/java/rife/bld/BldCache.java
@@ -271,6 +271,10 @@ public class BldCache {
finger_print.append(entry.getKey());
finger_print.append('\n');
if (entry.getValue() != null) {
+ for (var bom : entry.getValue().boms()) {
+ finger_print.append(bom.toString());
+ finger_print.append('\n');
+ }
for (var dependency : entry.getValue()) {
finger_print.append(dependency.toString());
finger_print.append('\n');
diff --git a/src/main/java/rife/bld/dependencies/Bom.java b/src/main/java/rife/bld/dependencies/Bom.java
new file mode 100644
index 0000000..e83bbe3
--- /dev/null
+++ b/src/main/java/rife/bld/dependencies/Bom.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ */
+package rife.bld.dependencies;
+
+import java.util.regex.Pattern;
+
+/**
+ * Contains the information required to describe a bill of materials (BOM)
+ * in the build system.
+ *
+ * A BOM is a POM whose dependency management section supplies versions
+ * during dependency resolution: dependencies that are declared without a
+ * version take the BOM's version, and transitive dependencies that match a
+ * BOM entry are pinned to the BOM's version. Versions that are explicitly
+ * declared in the build file, and versions provided through the
+ * {@code bld.override} property, always take precedence over a BOM.
+ *
+ * BOMs don't transfer any artifacts, they only participate in resolution.
+ *
+ * @author Geert Bevin (gbevin[remove] at uwyn dot com)
+ * @since 2.4.0
+ */
+public class Bom extends Dependency {
+ public Bom(String groupId, String artifactId) {
+ this(groupId, artifactId, null, null);
+ }
+
+ public Bom(String groupId, String artifactId, Version version) {
+ this(groupId, artifactId, version, null);
+ }
+
+ public Bom(String groupId, String artifactId, Version version, String classifier) {
+ super(groupId, artifactId, version, classifier, TYPE_BOM);
+ }
+
+ private static final Pattern BOM_PATTERN = Pattern.compile("^(?[^:@]+):(?[^:@]+)(?::(?[^:@]+)(?::(?[^:@]+))?)?(?:@(?:bom|pom))?$");
+
+ /**
+ * Parses a BOM from a string representation.
+ * The format is {@code groupId:artifactId:version:classifier}.
+ * The {@code version} and {@code classifier} are optional, and an
+ * optional {@code @bom} or {@code @pom} type suffix is accepted.
+ *
+ * If the string can't be successfully parsed, {@code null} will be returned.
+ *
+ * @param bom the BOM string to parse
+ * @return a parsed instance of {@code Bom}; or
+ * {@code null} when the string couldn't be parsed
+ * @since 2.4.0
+ */
+ public static Bom parse(String bom) {
+ if (bom == null || bom.isEmpty()) {
+ return null;
+ }
+
+ var matcher = BOM_PATTERN.matcher(bom);
+ if (!matcher.matches()) {
+ return null;
+ }
+
+ var groupId = matcher.group("groupId");
+ var artifactId = matcher.group("artifactId");
+ var version = Version.parse(matcher.group("version"));
+ var classifier = matcher.group("classifier");
+
+ return new Bom(groupId, artifactId, version, classifier);
+ }
+}
diff --git a/src/main/java/rife/bld/dependencies/Dependency.java b/src/main/java/rife/bld/dependencies/Dependency.java
index b03ab00..473e4a2 100644
--- a/src/main/java/rife/bld/dependencies/Dependency.java
+++ b/src/main/java/rife/bld/dependencies/Dependency.java
@@ -40,6 +40,14 @@ public class Dependency {
// see https://github.com/apache/maven/blob/maven-4.0.0-beta-3/api/maven-api-core/src/main/java/org/apache/maven/api/Type.java
public static final String TYPE_MODULAR_JAR = "modular-jar";
+ /**
+ * The dependency type name for a bill of materials POM whose
+ * dependency management supplies versions during resolution.
+ *
+ * @since 2.4.0
+ */
+ public static final String TYPE_BOM = "bom";
+
private final String groupId_;
private final String artifactId_;
private final Version version_;
diff --git a/src/main/java/rife/bld/dependencies/DependencyResolver.java b/src/main/java/rife/bld/dependencies/DependencyResolver.java
index 5400505..4caf358 100644
--- a/src/main/java/rife/bld/dependencies/DependencyResolver.java
+++ b/src/main/java/rife/bld/dependencies/DependencyResolver.java
@@ -102,7 +102,7 @@ public class DependencyResolver {
var pom_dependencies = getMavenPom(dependency_).getDependencies(scopes);
var result = new DependencySet();
for (var dependency : pom_dependencies) {
- result.add(resolution_.overrideDependency(dependency.convertToDependency()));
+ result.add(resolution_.overrideTransitiveDependency(dependency.convertToDependency()));
}
return result;
}
@@ -121,19 +121,17 @@ public class DependencyResolver {
* @since 1.5
*/
public DependencySet getAllDependencies(Scope... scopes) {
- var prefetcher = PomPrefetcher.create(resolution_, retriever_, repositories_);
+ var prefetcher = new PomPrefetcher(resolution_, retriever_, repositories_);
try {
return getAllDependencies(prefetcher, scopes);
} finally {
- if (prefetcher != null) {
- prefetcher.shutdown();
- }
+ prefetcher.shutdown();
}
}
DependencySet getAllDependencies(PomPrefetcher prefetcher, Scope... scopes) {
var result = new DependencySet();
- var overridden = resolution_.overrideDependency(dependency_);
+ var overridden = resolution_.overrideDeclaredDependency(dependency_);
result.add(overridden);
var dependency_queue = new ArrayList();
@@ -151,11 +149,9 @@ public class DependencyResolver {
dependency_queue.addAll(next_dependencies);
// speculatively retrieve the POMs of the queued dependencies in
// parallel so that they are cached when they're processed in order
- if (prefetcher != null) {
- prefetcher.prefetch(next_dependencies);
- }
+ prefetcher.prefetch(next_dependencies);
- // unless we find a next set of dependencies to add, stop resolving
+ // unless we find the next set of dependencies to add, stop resolving
parent = null;
next_dependencies = null;
@@ -163,7 +159,7 @@ public class DependencyResolver {
// part of the results yet
while (!dependency_queue.isEmpty()) {
var candidate = dependency_queue.remove(0);
- var dependency = resolution_.overrideDependency(candidate.convertToDependency());
+ var dependency = resolution_.overrideTransitiveDependency(candidate.convertToDependency());
if (!result.contains(dependency)) {
result.add(dependency);
diff --git a/src/main/java/rife/bld/dependencies/DependencyScopes.java b/src/main/java/rife/bld/dependencies/DependencyScopes.java
index d93e277..fa989e2 100644
--- a/src/main/java/rife/bld/dependencies/DependencyScopes.java
+++ b/src/main/java/rife/bld/dependencies/DependencyScopes.java
@@ -147,14 +147,16 @@ public class DependencyScopes extends LinkedHashMap {
}
private DependencySet resolveScopedDependencies(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories, Scope[] resolvedScopes, Scope[] transitiveScopes, DependencySet excluded) {
- var resolution = new VersionResolution(properties);
var roots = new ArrayList();
+ var boms = new ArrayList();
for (var scope : resolvedScopes) {
var scoped_dependencies = get(scope);
if (scoped_dependencies != null) {
roots.addAll(scoped_dependencies);
+ boms.addAll(scoped_dependencies.boms());
}
}
+ var resolution = new VersionResolution(properties, retriever, repositories, boms);
var dependencies = new ParallelDependencyResolver(resolution, retriever, repositories).resolveAllDependencies(roots, transitiveScopes);
if (excluded != null) {
dependencies.removeAll(excluded);
diff --git a/src/main/java/rife/bld/dependencies/DependencySet.java b/src/main/java/rife/bld/dependencies/DependencySet.java
index a654b6e..d5537ed 100644
--- a/src/main/java/rife/bld/dependencies/DependencySet.java
+++ b/src/main/java/rife/bld/dependencies/DependencySet.java
@@ -24,6 +24,7 @@ public class DependencySet extends AbstractSet implements Set dependencies_ = new LinkedHashMap<>();
private final Set localDependencies_ = new LinkedHashSet<>();
private final Set localModules_ = new LinkedHashSet<>();
+ private final Set boms_ = new LinkedHashSet<>();
/**
* Creates an empty dependency set.
@@ -81,7 +82,32 @@ public class DependencySet extends AbstractSet implements Set
+ * BOMs aren't transferred, their dependency management sections supply
+ * versions during the resolution of this dependency set.
+ *
+ * @param bom the BOM to include
+ * @return this dependency set instance
+ * @since 2.4.0
+ */
+ public DependencySet include(Bom bom) {
+ boms_.add(bom);
+ return this;
+ }
+
+ /**
+ * Retrieves the bills of materials.
+ *
+ * @return the set of BOMs
+ * @since 2.4.0
+ */
+ public Set boms() {
+ return boms_;
+ }
+
+ /**
+ * Includes a local module in the dependency set.
*
* Local modules aren't resolved and point to a location on
* the file system.
@@ -153,7 +179,7 @@ public class DependencySet extends AbstractSet implements Set
- * The version can be different from the dependency passed in and this
+ * The version can be different from the dependency passed in, and this
* method can be used to look up the actual version of the dependency in the set.
*
* @param dependency the dependency to look for
@@ -178,6 +204,7 @@ public class DependencySet extends AbstractSet implements Set repositories, Scope... scopes) {
+ resolution = resolution.withBoms(retriever, repositories, boms_);
return new ParallelDependencyResolver(resolution, retriever, repositories).resolveAllDependencies(this, scopes).generateDependencyTree();
}
diff --git a/src/main/java/rife/bld/dependencies/ParallelDependencyResolver.java b/src/main/java/rife/bld/dependencies/ParallelDependencyResolver.java
index ba1a7e2..9cf07d4 100644
--- a/src/main/java/rife/bld/dependencies/ParallelDependencyResolver.java
+++ b/src/main/java/rife/bld/dependencies/ParallelDependencyResolver.java
@@ -63,7 +63,7 @@ public class ParallelDependencyResolver {
return result;
}
- var prefetcher = PomPrefetcher.create(resolution_, retriever_, repositories_);
+ var prefetcher = new PomPrefetcher(resolution_, retriever_, repositories_);
try {
var resolutions = new ArrayList>(roots.size());
for (var root : roots) {
@@ -73,9 +73,7 @@ public class ParallelDependencyResolver {
result.addAll(dependencies);
}
} finally {
- if (prefetcher != null) {
- prefetcher.shutdown();
- }
+ prefetcher.shutdown();
}
return result;
}
diff --git a/src/main/java/rife/bld/dependencies/PomPrefetcher.java b/src/main/java/rife/bld/dependencies/PomPrefetcher.java
index a0ad475..1a6fa77 100644
--- a/src/main/java/rife/bld/dependencies/PomPrefetcher.java
+++ b/src/main/java/rife/bld/dependencies/PomPrefetcher.java
@@ -18,6 +18,10 @@ import java.util.concurrent.Executors;
*
* A single instance can be shared by multiple concurrent resolutions, the
* POM of each unique dependency will only be prefetched once.
+ *
+ * Prefetching only has benefits when the retrieved POMs are cached for the
+ * sequential resolution that follows, the prefetcher is inert when the
+ * retriever doesn't cache or when the resolution parallelism disables it.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 2.4.0
@@ -29,32 +33,23 @@ class PomPrefetcher {
private final ExecutorService executor_;
private final Set submitted_ = ConcurrentHashMap.newKeySet();
- /**
- * Creates a prefetcher when it can be beneficial.
- *
- * @return the prefetcher; or {@code null} when the retriever doesn't
- * cache the retrieved POMs or when the resolution parallelism disables it
- * @since 2.4.0
- */
- static PomPrefetcher create(VersionResolution resolution, ArtifactRetriever retriever, List repositories) {
- // prefetching only has benefits when the retrieved POMs are
- // cached for the sequential resolution that follows
- if (!retriever.isCaching() || resolution.resolutionParallelism() <= 1) {
- return null;
- }
- return new PomPrefetcher(resolution, retriever, repositories);
- }
-
- private PomPrefetcher(VersionResolution resolution, ArtifactRetriever retriever, List repositories) {
+ PomPrefetcher(VersionResolution resolution, ArtifactRetriever retriever, List repositories) {
resolution_ = resolution;
retriever_ = retriever;
repositories_ = repositories;
- executor_ = Executors.newFixedThreadPool(resolution.resolutionParallelism());
+ if (retriever.isCaching() && resolution.resolutionParallelism() > 1) {
+ executor_ = Executors.newFixedThreadPool(resolution.resolutionParallelism());
+ } else {
+ executor_ = null;
+ }
}
void prefetch(Collection candidates) {
+ if (executor_ == null) {
+ return;
+ }
for (var candidate : candidates) {
- var dependency = resolution_.overrideDependency(candidate.convertToDependency());
+ var dependency = resolution_.overrideTransitiveDependency(candidate.convertToDependency());
if (submitted_.add(dependency)) {
executor_.submit(() -> {
try {
@@ -70,6 +65,8 @@ class PomPrefetcher {
}
void shutdown() {
- executor_.shutdownNow();
+ if (executor_ != null) {
+ executor_.shutdownNow();
+ }
}
}
diff --git a/src/main/java/rife/bld/dependencies/VersionResolution.java b/src/main/java/rife/bld/dependencies/VersionResolution.java
index 998e77a..409009b 100644
--- a/src/main/java/rife/bld/dependencies/VersionResolution.java
+++ b/src/main/java/rife/bld/dependencies/VersionResolution.java
@@ -6,14 +6,17 @@ package rife.bld.dependencies;
import rife.ioc.HierarchicalProperties;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
- * This class is responsible for managing version overrides for dependencies.
+ * This class is responsible for managing the versions that are applied
+ * during dependency resolution.
*
- * It allows users to specify a property keys with the prefix "{@code bld.override}" where the values will be parsed as
+ * It allows users to specify a property key with the prefix "{@code bld.override}" where the values will be parsed as
* a comma-separated list of dependencies with the versions that should override any other versions that are encountered.
*
* For instance:
@@ -29,8 +32,18 @@ import java.util.logging.Logger;
* bld.override-h2=com.h2database:h2:2.2.222
*
*
+ * It can also import versions from the dependency management sections of
+ * bills of materials (BOMs). When resolving versions, the
+ * following precedence applies: a version from a "{@code bld.override}"
+ * property always wins, followed by a version explicitly declared
+ * in the build file, followed by a version from a BOM. Dependencies that
+ * are declared without a version and that are not covered by a BOM,
+ * resolve to their latest version.
+ *
* It also captures other dependency resolution preferences, like the number
* of parallel artifact transfers through the "{@code bld.transferParallelism}"
+ * property and the number of parallel POM retrievals during transitive
+ * dependency resolution through the "{@code bld.resolutionParallelism}"
* property.
* @since 2.0
*/
@@ -59,6 +72,7 @@ public class VersionResolution {
private static final int DEFAULT_RESOLUTION_PARALLELISM = 6;
private final Map versionOverrides_ = new HashMap<>();
+ private final Map bomVersions_;
private final int transferParallelism_;
private final int resolutionParallelism_;
@@ -100,6 +114,61 @@ public class VersionResolution {
}
transferParallelism_ = parseParallelism(properties, PROPERTY_TRANSFER_PARALLELISM, DEFAULT_TRANSFER_PARALLELISM);
resolutionParallelism_ = parseParallelism(properties, PROPERTY_RESOLUTION_PARALLELISM, DEFAULT_RESOLUTION_PARALLELISM);
+ bomVersions_ = Map.of();
+ }
+
+ private VersionResolution(VersionResolution base, Map bomVersions) {
+ versionOverrides_.putAll(base.versionOverrides_);
+ transferParallelism_ = base.transferParallelism_;
+ resolutionParallelism_ = base.resolutionParallelism_;
+ bomVersions_ = Map.copyOf(bomVersions);
+ }
+
+ /**
+ * Creates a new instance of the {@code VersionResolution} class from
+ * hierarchical properties and bills of materials whose dependency
+ * management sections supply versions during resolution.
+ *
+ * The BOMs are imported in the order they're provided, the first BOM
+ * that manages a particular dependency determines its version. The
+ * version overrides of the properties take precedence over any BOM
+ * version, they also apply when resolving the BOMs themselves.
+ *
+ * @param properties the hierarchical properties that will be used to determine the version overrides
+ * @param retriever the retriever to use to get the BOMs
+ * @param repositories the repositories to resolve the BOMs in
+ * @param boms the BOMs to import
+ * @since 2.4.0
+ */
+ public VersionResolution(HierarchicalProperties properties, ArtifactRetriever retriever, List repositories, Collection boms) {
+ this(new VersionResolution(properties), retriever, repositories, boms);
+ }
+
+ private VersionResolution(VersionResolution base, ArtifactRetriever retriever, List repositories, Collection boms) {
+ this(base, resolveBomVersions(base, retriever, repositories, boms));
+ }
+
+ VersionResolution withBoms(ArtifactRetriever retriever, List repositories, Collection boms) {
+ if (boms == null || boms.isEmpty()) {
+ return this;
+ }
+ return new VersionResolution(this, retriever, repositories, boms);
+ }
+
+ private static Map resolveBomVersions(VersionResolution resolution, ArtifactRetriever retriever, List repositories, Collection boms) {
+ var bom_versions = new HashMap();
+ if (boms != null) {
+ for (var bom : boms) {
+ var pom = new DependencyResolver(resolution, retriever, repositories, bom).getMavenPom(bom);
+ for (var managed : pom.getManagedDependencies()) {
+ if (managed.version() != null && !managed.version().isBlank()) {
+ var dependency = managed.convertToDependency();
+ bom_versions.putIfAbsent(dependency.toArtifactString(), dependency.version());
+ }
+ }
+ }
+ }
+ return bom_versions;
}
private static int parseParallelism(HierarchicalProperties properties, String property, int defaultValue) {
@@ -125,10 +194,16 @@ public class VersionResolution {
*/
public Version overrideVersion(Dependency original) {
var overridden = versionOverrides_.get(original.toArtifactString());
- if (overridden == null) {
- return original.version();
+ if (overridden != null) {
+ return overridden;
}
- return overridden;
+ if (VersionNumber.UNKNOWN.equals(original.version())) {
+ var bom_version = bomVersions_.get(original.toArtifactString());
+ if (bom_version != null) {
+ return bom_version;
+ }
+ }
+ return original.version();
}
/**
@@ -144,9 +219,63 @@ public class VersionResolution {
if (overridden == null) {
return original;
}
+ return withVersion(original, overridden);
+ }
+
+ /**
+ * Applies version overrides to a dependency that was explicitly declared
+ * in the build file.
+ *
+ * A version from the {@code bld.override} property always takes
+ * precedence. A bill of materials version is only applied when the
+ * dependency was declared without a version, an explicitly declared
+ * version is never rewritten by a BOM.
+ *
+ * @param declared the declared dependency to apply overrides to
+ * @return the dependency with the overridden version if one applies; or
+ * the original dependency otherwise
+ * @since 2.4.0
+ */
+ public Dependency overrideDeclaredDependency(Dependency declared) {
+ var overridden = versionOverrides_.get(declared.toArtifactString());
+ if (overridden == null && VersionNumber.UNKNOWN.equals(declared.version())) {
+ overridden = bomVersions_.get(declared.toArtifactString());
+ }
+ if (overridden == null) {
+ return declared;
+ }
+ return withVersion(declared, overridden);
+ }
+
+ /**
+ * Applies version overrides to a transitive dependency that was
+ * encountered during resolution.
+ *
+ * A version from the {@code bld.override} property always takes
+ * precedence, followed by a matching bill of materials version that
+ * pins the transitive dependency regardless of the version its parent
+ * POM declared.
+ *
+ * @param transitive the transitive dependency to apply overrides to
+ * @return the dependency with the overridden version if one applies; or
+ * the original dependency otherwise
+ * @since 2.4.0
+ */
+ public Dependency overrideTransitiveDependency(Dependency transitive) {
+ var overridden = versionOverrides_.get(transitive.toArtifactString());
+ if (overridden == null) {
+ overridden = bomVersions_.get(transitive.toArtifactString());
+ }
+ if (overridden == null) {
+ return transitive;
+ }
+ return withVersion(transitive, overridden);
+ }
+
+ private static Dependency withVersion(Dependency original, Version version) {
return new Dependency(original.groupId(),
original.artifactId(),
- overridden,
+ version,
original.classifier(),
original.type(),
original.exclusions(),
@@ -163,6 +292,18 @@ public class VersionResolution {
return versionOverrides_;
}
+ /**
+ * Returns the map of versions that were imported from bills of
+ * materials, where the key is the name of the dependency and the value
+ * is the managed version.
+ *
+ * @return the map of BOM versions
+ * @since 2.4.0
+ */
+ public Map bomVersions() {
+ return bomVersions_;
+ }
+
/**
* Returns the number of artifact transfers that are performed in parallel,
* {@code 1} means transfers are sequential.
diff --git a/src/main/java/rife/bld/dependencies/Xml2MavenPom.java b/src/main/java/rife/bld/dependencies/Xml2MavenPom.java
index 3fa7296..45136c7 100644
--- a/src/main/java/rife/bld/dependencies/Xml2MavenPom.java
+++ b/src/main/java/rife/bld/dependencies/Xml2MavenPom.java
@@ -129,6 +129,17 @@ class Xml2MavenPom extends Xml2Data {
return result;
}
+ Set getManagedDependencies() {
+ var result = new LinkedHashSet();
+ // iterate the values since putting an entry with an equal key
+ // preserves the original key instance while the value reflects
+ // the actual managed dependency
+ for (var managed : dependencyManagement_.values()) {
+ result.add(resolveDependency(managed));
+ }
+ return result;
+ }
+
PomDependency resolveDependency(PomDependency dependency) {
return new PomDependency(
resolveMavenProperties(dependency.groupId()),
diff --git a/src/main/java/rife/bld/operations/UpdatesOperation.java b/src/main/java/rife/bld/operations/UpdatesOperation.java
index 67634b6..04c8ff5 100644
--- a/src/main/java/rife/bld/operations/UpdatesOperation.java
+++ b/src/main/java/rife/bld/operations/UpdatesOperation.java
@@ -35,7 +35,21 @@ public class UpdatesOperation extends AbstractOperation {
var scopes = new ArrayList();
var dependencies = new ArrayList();
for (var entry : dependencies_.entrySet()) {
- for (var dependency : entry.getValue()) {
+ var scoped_dependencies = entry.getValue();
+ for (var bom : scoped_dependencies.boms()) {
+ scopes.add(entry.getKey());
+ dependencies.add(bom);
+ }
+
+ // dependencies that are declared without a version and that are
+ // covered by a BOM in the same scope have their version governed
+ // by that BOM, the BOM update itself carries their update signal
+ var scope_resolution = new VersionResolution(properties(), artifactRetriever(), repositories(), scoped_dependencies.boms());
+ for (var dependency : scoped_dependencies) {
+ if (VersionNumber.UNKNOWN.equals(dependency.version()) &&
+ scope_resolution.bomVersions().containsKey(dependency.toArtifactString())) {
+ continue;
+ }
scopes.add(entry.getKey());
dependencies.add(dependency);
}
@@ -48,9 +62,13 @@ public class UpdatesOperation extends AbstractOperation {
var dependency = dependencies.get(i);
var latest = latest_versions.get(i);
if (latest.compareTo(dependency.version()) > 0) {
- var latest_dependency = new Dependency(dependency.groupId(), dependency.artifactId(), latest,
- dependency.classifier(), dependency.type());
- result.scope(scopes.get(i)).include(latest_dependency);
+ if (dependency instanceof Bom) {
+ result.scope(scopes.get(i)).include(new Bom(dependency.groupId(), dependency.artifactId(), latest,
+ dependency.classifier()));
+ } else {
+ result.scope(scopes.get(i)).include(new Dependency(dependency.groupId(), dependency.artifactId(), latest,
+ dependency.classifier(), dependency.type()));
+ }
}
}
@@ -63,6 +81,9 @@ public class UpdatesOperation extends AbstractOperation {
for (var entry : result.entrySet()) {
var scope = entry.getKey();
System.out.println(scope + ":");
+ for (var bom : entry.getValue().boms()) {
+ System.out.println(" " + bom);
+ }
for (var dependency : entry.getValue()) {
System.out.println(" " + dependency);
}
diff --git a/src/test/java/rife/bld/dependencies/TestBom.java b/src/test/java/rife/bld/dependencies/TestBom.java
new file mode 100644
index 0000000..726acf7
--- /dev/null
+++ b/src/test/java/rife/bld/dependencies/TestBom.java
@@ -0,0 +1,637 @@
+/*
+ * Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com)
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ */
+package rife.bld.dependencies;
+
+import com.sun.net.httpserver.HttpServer;
+import org.junit.jupiter.api.Test;
+import rife.bld.BldCache;
+import rife.bld.dependencies.exceptions.ArtifactNotFoundException;
+import rife.ioc.HierarchicalProperties;
+import rife.tools.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static rife.bld.dependencies.RepositoryTestHelper.getNextRepositories;
+import static rife.bld.dependencies.Scope.compile;
+
+public class TestBom {
+ @Test
+ void testParse() {
+ assertEquals(new Bom("com.example", "bom1"), Bom.parse("com.example:bom1"));
+ assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3)), Bom.parse("com.example:bom1:1.2.3"));
+ assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3)), Bom.parse("com.example:bom1:1.2.3@bom"));
+ assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3)), Bom.parse("com.example:bom1:1.2.3@pom"));
+ assertEquals(new Bom("com.example", "bom1", new VersionNumber(1, 2, 3), "classifier1"), Bom.parse("com.example:bom1:1.2.3:classifier1"));
+ assertNull(Bom.parse(null));
+ assertNull(Bom.parse(""));
+ assertNull(Bom.parse("not@a@bom"));
+ }
+
+ @Test
+ void testBomFillsDeclaredVersion() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "a:1.4.0", pom("a", "1.4.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("1.4.0"), resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomPinsTransitiveDependency() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("b", "1.5.0")),
+ "a:1.0.0", pom("a", "1.0.0", dependency("b", "2.0.0")),
+ "b:1.5.0", pom("b", "1.5.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("1.5.0"), resolved.get(new Dependency("com.example", "b")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testExplicitDeclaredVersionWinsOverBom() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "a:3.0.0", pom("a", "3.0.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a", new VersionNumber(3, 0, 0)));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("3.0.0"), resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testVersionOverridePropertyWinsOverBomForTransitive() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("b", "1.5.0")),
+ "a:1.0.0", pom("a", "1.0.0", dependency("b", "2.0.0")),
+ "b:2.5.0", pom("b", "2.5.0", "")));
+ server.start();
+ try {
+ var properties = new HierarchicalProperties();
+ properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:b:2.5.0");
+
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+
+ var resolved = scopes.resolveCompileDependencies(properties, ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("2.5.0"), resolved.get(new Dependency("com.example", "b")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testVersionOverridePropertyWinsOverBomForDeclared() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "a:2.5.0", pom("a", "2.5.0", "")));
+ server.start();
+ try {
+ var properties = new HierarchicalProperties();
+ properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:a:2.5.0");
+
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var resolved = scopes.resolveCompileDependencies(properties, ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("2.5.0"), resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testVersionOverridePropertyAppliesToBomItself() throws Exception {
+ // only version 2.0.0 of the BOM is served, resolution succeeds
+ // solely when the override rewrites the declared BOM version
+ var server = createArtifactServer(Map.of(
+ "bom1:2.0.0", bomPom("bom1", "2.0.0", managed("a", "1.4.0")),
+ "a:1.4.0", pom("a", "1.4.0", "")));
+ server.start();
+ try {
+ var properties = new HierarchicalProperties();
+ properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:bom1:2.0.0");
+
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var resolved = scopes.resolveCompileDependencies(properties, ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("1.4.0"), resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testFirstBomWinsOnConflict() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("c", "1.0.0")),
+ "bom2:1.0.0", bomPom("bom2", "1.0.0", managed("c", "2.0.0")),
+ "c:1.0.0", pom("c", "1.0.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Bom("com.example", "bom2", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "c"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("1.0.0"), resolved.get(new Dependency("com.example", "c")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testNestedBomImport() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0",
+ managed("d", "1.0.0") +
+ "com.examplebom21.0.0pomimport"),
+ "bom2:1.0.0", bomPom("bom2", "1.0.0", managed("d", "9.9.9") + managed("e", "2.0.0")),
+ "d:1.0.0", pom("d", "1.0.0", ""),
+ "e:2.0.0", pom("e", "2.0.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "d"))
+ .include(new Dependency("com.example", "e"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ // the importing BOM's own entry wins over the imported entry
+ assertEquals(Version.parse("1.0.0"), resolved.get(new Dependency("com.example", "d")).version());
+ // entries that are only imported apply as well
+ assertEquals(Version.parse("2.0.0"), resolved.get(new Dependency("com.example", "e")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomParentPomInheritance() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bomparent:1.0.0", bomPom("bomparent", "1.0.0", managed("a", "1.4.0") + managed("b", "9.9.9")),
+ "bom1:1.0.0", bomPomWithParent("bom1", "1.0.0", "bomparent", managed("b", "1.5.0")),
+ "a:1.4.0", pom("a", "1.4.0", ""),
+ "b:1.5.0", pom("b", "1.5.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"))
+ .include(new Dependency("com.example", "b"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ // entries inherited from the BOM's parent POM apply
+ assertEquals(Version.parse("1.4.0"), resolved.get(new Dependency("com.example", "a")).version());
+ // the BOM's own entry wins over its parent's entry
+ assertEquals(Version.parse("1.5.0"), resolved.get(new Dependency("com.example", "b")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomPropertyPlaceholders() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", """
+
+
+ 4.0.0
+ com.example
+ bom1
+ 1.0.0
+ pom
+
+ 1.4.0
+
+
+
+ com.examplea${dep.version}
+
+
+ """,
+ "a:1.4.0", pom("a", "1.4.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("1.4.0"), resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testUncoveredVersionlessStillResolvesLatest() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "a:1.4.0", pom("a", "1.4.0", ""),
+ "f:2.2.0", pom("f", "2.2.0", "")),
+ Map.of("f", metadata("f", "2.2.0", "1.0.0", "2.2.0")));
+ server.start();
+ try {
+ var retriever = ArtifactRetriever.cachingInstance();
+ var repositories = serverRepositories(server);
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"))
+ .include(new Dependency("com.example", "f"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), retriever, repositories);
+ // covered by the BOM : the BOM version applies
+ assertEquals(Version.parse("1.4.0"), resolved.get(new Dependency("com.example", "a")).version());
+ // not covered by the BOM : the version stays unknown in the set,
+ // exactly as it would without any BOM declared
+ assertEquals(VersionNumber.UNKNOWN, resolved.get(new Dependency("com.example", "f")).version());
+ // and it still resolves to the latest version from the metadata
+ var resolution = new VersionResolution(new HierarchicalProperties())
+ .withBoms(retriever, repositories, List.of(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0))));
+ assertEquals(Version.parse("2.2.0"), new DependencyResolver(resolution, retriever, repositories, new Dependency("com.example", "f")).resolveVersion());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomScopeIsolation() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "a:1.4.0", pom("a", "1.4.0", ""),
+ "a:2.2.0", pom("a", "2.2.0", "")),
+ Map.of("a", metadata("a", "2.2.0", "1.4.0", "2.2.0")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+ scopes.scope(Scope.test)
+ .include(new Dependency("com.example", "a"));
+
+ // the compile scope BOM applies to the compile scope resolution
+ var compile_resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("1.4.0"), compile_resolved.get(new Dependency("com.example", "a")).version());
+
+ // and doesn't leak into the test scope resolution, whose version
+ // stays unknown and falls back to the latest from the metadata
+ var test_resolved = scopes.resolveTestDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(VersionNumber.UNKNOWN, test_resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testMissingBomFailsLoudly() throws Exception {
+ var server = createArtifactServer(Map.of());
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "missing", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+
+ assertThrows(ArtifactNotFoundException.class,
+ () -> scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server)));
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testManagedEntryWithoutVersionIsIgnored() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0",
+ "com.examplex" +
+ managed("a", "1.4.0")),
+ "a:1.4.0", pom("a", "1.4.0", "")));
+ server.start();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ assertEquals(Version.parse("1.4.0"), resolved.get(new Dependency("com.example", "a")).version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomIsNotTransferred() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0")),
+ "a:1.4.0", pom("a", "1.4.0", "")));
+ server.start();
+ var tmp = Files.createTempDirectory("bomtransfers").toFile();
+ try {
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ var resolution = new VersionResolution(new HierarchicalProperties());
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), serverRepositories(server));
+ resolved.transferIntoDirectory(resolution, ArtifactRetriever.cachingInstance(), serverRepositories(server), tmp, tmp);
+
+ assertTrue(new File(tmp, "a-1.4.0.jar").exists());
+ assertEquals(1, tmp.list().length, "expected only the dependency artifact, found: " + List.of(tmp.list()));
+ } finally {
+ server.stop(0);
+ FileUtils.deleteDirectory(tmp);
+ }
+ }
+
+ @Test
+ void testGenerateTransitiveDependencyTreeWithBom() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("b", "1.5.0")),
+ "a:1.0.0", pom("a", "1.0.0", dependency("b", "2.0.0")),
+ "b:1.5.0", pom("b", "1.5.0", "")));
+ server.start();
+ try {
+ var dependencies = new DependencySet();
+ dependencies.include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)));
+ dependencies.include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+
+ var tree = dependencies.generateTransitiveDependencyTree(new VersionResolution(null), ArtifactRetriever.cachingInstance(), serverRepositories(server), compile);
+ assertTrue(tree.contains("com.example:a:1.0.0"), "tree was:\n" + tree);
+ assertTrue(tree.contains("com.example:b:1.5.0"), "tree was:\n" + tree);
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testVersionResolutionBomSemantics() throws Exception {
+ var server = createArtifactServer(Map.of(
+ "bom1:1.0.0", bomPom("bom1", "1.0.0", managed("a", "1.4.0"))));
+ server.start();
+ try {
+ var properties = new HierarchicalProperties();
+ properties.put(VersionResolution.PROPERTY_OVERRIDE_PREFIX, "com.example:o:5.0.0");
+ properties.put(VersionResolution.PROPERTY_TRANSFER_PARALLELISM, "3");
+ var base = new VersionResolution(properties);
+
+ // no BOMs returns the same instance
+ assertSame(base, base.withBoms(ArtifactRetriever.cachingInstance(), serverRepositories(server), List.of()));
+ assertSame(base, base.withBoms(ArtifactRetriever.cachingInstance(), serverRepositories(server), null));
+
+ var resolution = base.withBoms(ArtifactRetriever.cachingInstance(), serverRepositories(server),
+ List.of(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0))));
+
+ // the public constructor imports BOMs identically
+ var constructed = new VersionResolution(properties, ArtifactRetriever.cachingInstance(), serverRepositories(server),
+ List.of(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0))));
+ assertEquals(resolution.bomVersions(), constructed.bomVersions());
+ assertEquals(resolution.versionOverrides(), constructed.versionOverrides());
+ assertEquals(resolution.transferParallelism(), constructed.transferParallelism());
+
+ // the derived instance preserves the overrides and parallelism settings
+ assertNotSame(base, resolution);
+ assertEquals(base.versionOverrides(), resolution.versionOverrides());
+ assertEquals(3, resolution.transferParallelism());
+ assertEquals(Map.of("com.example:a", Version.parse("1.4.0")), resolution.bomVersions());
+
+ // a declared dependency without version is filled in from the BOM
+ assertEquals(Version.parse("1.4.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "a")).version());
+ // a declared dependency with an explicit version is never rewritten by the BOM
+ assertEquals(Version.parse("3.0.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "a", new VersionNumber(3, 0, 0))).version());
+ // a transitive dependency is pinned by the BOM even when it has a version
+ assertEquals(Version.parse("1.4.0"), resolution.overrideTransitiveDependency(new Dependency("com.example", "a", new VersionNumber(2, 0, 0))).version());
+ // the version override property beats the BOM
+ assertEquals(Version.parse("5.0.0"), resolution.overrideDeclaredDependency(new Dependency("com.example", "o")).version());
+ // version resolution consults the BOM for unknown versions only
+ assertEquals(Version.parse("1.4.0"), resolution.overrideVersion(new Dependency("com.example", "a")));
+ assertEquals(Version.parse("2.0.0"), resolution.overrideVersion(new Dependency("com.example", "a", new VersionNumber(2, 0, 0))));
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testBomChangesInvalidateDependenciesCache() throws Exception {
+ var tmp = Files.createTempDirectory("bomcache").toFile();
+ try {
+ var repositories = List.of(new Repository("http://localhost/"));
+
+ var scopes_without_bom = new DependencyScopes();
+ scopes_without_bom.scope(compile).include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+
+ var scopes_with_bom = new DependencyScopes();
+ scopes_with_bom.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+
+ var cache = new BldCache(tmp, VersionResolution.dummy());
+ cache.cacheDependenciesHash(repositories, scopes_without_bom);
+ cache.writeCache();
+
+ // the same dependencies without BOM validate against the cache
+ var cache_same = new BldCache(tmp, VersionResolution.dummy());
+ cache_same.cacheDependenciesHash(repositories, scopes_without_bom);
+ assertTrue(cache_same.isDependenciesHashValid());
+
+ // adding a BOM invalidates the cache
+ var cache_bom = new BldCache(tmp, VersionResolution.dummy());
+ cache_bom.cacheDependenciesHash(repositories, scopes_with_bom);
+ assertFalse(cache_bom.isDependenciesHashValid());
+
+ // a different BOM version invalidates the cache too
+ cache_bom.writeCache();
+ var scopes_with_other_bom = new DependencyScopes();
+ scopes_with_other_bom.scope(compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(2, 0, 0)))
+ .include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+ var cache_other_bom = new BldCache(tmp, VersionResolution.dummy());
+ cache_other_bom.cacheDependenciesHash(repositories, scopes_with_other_bom);
+ assertFalse(cache_other_bom.isDependenciesHashValid());
+ } finally {
+ FileUtils.deleteDirectory(tmp);
+ }
+ }
+
+ @Test
+ void testBomVertxAcceptance() {
+ // reproduction of the scenario in https://github.com/rife2/bld/issues/59
+ var scopes = new DependencyScopes();
+ scopes.scope(compile)
+ .include(new Bom("io.vertx", "vertx-stack-depchain", new VersionNumber(4, 5, 12)))
+ .include(new Dependency("io.vertx", "vertx-core"))
+ .include(new Dependency("com.fasterxml.jackson.core", "jackson-databind"));
+
+ var resolved = scopes.resolveCompileDependencies(new HierarchicalProperties(), ArtifactRetriever.cachingInstance(), getNextRepositories());
+ assertEquals(Version.parse("4.5.12"), resolved.get(new Dependency("io.vertx", "vertx-core")).version());
+ assertEquals(Version.parse("2.16.1"), resolved.get(new Dependency("com.fasterxml.jackson.core", "jackson-databind")).version());
+ assertEquals(Version.parse("2.16.1"), resolved.get(new Dependency("com.fasterxml.jackson.core", "jackson-core")).version());
+ }
+
+ private static List serverRepositories(HttpServer server) {
+ return List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/"));
+ }
+
+ private static HttpServer createArtifactServer(Map poms)
+ throws IOException {
+ return createArtifactServer(poms, Map.of());
+ }
+
+ private static HttpServer createArtifactServer(Map poms, Map metadata)
+ throws IOException {
+ var server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+ server.createContext("/", exchange -> {
+ var segments = exchange.getRequestURI().getPath().split("/");
+ var filename = segments[segments.length - 1];
+ byte[] body = null;
+ if (filename.endsWith(".pom") && segments.length >= 3) {
+ var content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
+ if (content != null) {
+ body = content.getBytes();
+ }
+ } else if (filename.endsWith(".jar") && segments.length >= 3) {
+ if (poms.containsKey(segments[segments.length - 3] + ":" + segments[segments.length - 2])) {
+ body = "jar".getBytes();
+ }
+ } else if (filename.equals("maven-metadata.xml") && segments.length >= 2) {
+ var content = metadata.get(segments[segments.length - 2]);
+ if (content != null) {
+ body = content.getBytes();
+ }
+ }
+ if (body == null) {
+ exchange.sendResponseHeaders(404, -1);
+ } else {
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ }
+ exchange.close();
+ });
+ server.setExecutor(Executors.newCachedThreadPool());
+ return server;
+ }
+
+ private static String dependency(String artifact, String version) {
+ return "com.example" + artifact + "" + version + "";
+ }
+
+ private static String managed(String artifact, String version) {
+ return dependency(artifact, version);
+ }
+
+ private static String pom(String artifact, String version, String dependencies) {
+ return """
+
+
+ 4.0.0
+ com.example
+ %s
+ %s
+ %s
+ """.formatted(artifact, version, dependencies);
+ }
+
+ private static String bomPom(String artifact, String version, String managedDependencies) {
+ return """
+
+
+ 4.0.0
+ com.example
+ %s
+ %s
+ pom
+
+ %s
+
+ """.formatted(artifact, version, managedDependencies);
+ }
+
+ private static String bomPomWithParent(String artifact, String version, String parentArtifact, String managedDependencies) {
+ return """
+
+
+ 4.0.0
+
+ com.example
+ %s
+ 1.0.0
+
+ com.example
+ %s
+ %s
+ pom
+
+ %s
+
+ """.formatted(parentArtifact, artifact, version, managedDependencies);
+ }
+
+ private static String metadata(String artifact, String latest, String... versions) {
+ var versions_xml = new StringBuilder();
+ for (var version : versions) {
+ versions_xml.append("").append(version).append("");
+ }
+ return """
+
+
+ com.example
+ %s
+
+ %s
+ %s
+ %s
+
+ """.formatted(artifact, latest, latest, versions_xml);
+ }
+}
diff --git a/src/test/java/rife/bld/operations/TestUpdatesOperation.java b/src/test/java/rife/bld/operations/TestUpdatesOperation.java
index cf4dc15..175ea94 100644
--- a/src/test/java/rife/bld/operations/TestUpdatesOperation.java
+++ b/src/test/java/rife/bld/operations/TestUpdatesOperation.java
@@ -4,14 +4,18 @@
*/
package rife.bld.operations;
+import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.Test;
import rife.bld.WebProject;
import rife.bld.dependencies.*;
import rife.tools.FileUtils;
import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.util.List;
+import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@@ -168,4 +172,123 @@ public class TestUpdatesOperation {
FileUtils.deleteDirectory(tmp);
}
}
+
+ @Test
+ void testBomUpdateReported()
+ throws Exception {
+ var server = createArtifactServer(
+ Map.of("bom1:1.0.0", bomPom("bom1", "1.0.0", "com.examplea1.4.0")),
+ Map.of("bom1", metadata("bom1", "2.0.0", "1.0.0", "2.0.0"),
+ "a", metadata("a", "9.9.9", "1.4.0", "9.9.9")));
+ server.start();
+ try {
+ var operation = new UpdatesOperation()
+ .artifactRetriever(ArtifactRetriever.cachingInstance())
+ .repositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")));
+ operation.dependencies().scope(Scope.compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a"));
+
+ operation.execute();
+
+ // the newer BOM version is reported
+ var boms = operation.updates().scope(Scope.compile).boms();
+ assertEquals(1, boms.size());
+ assertEquals(new Bom("com.example", "bom1", new VersionNumber(2, 0, 0)), boms.iterator().next());
+
+ // the version-less dependency covered by the BOM is not reported,
+ // even though newer versions exist in the metadata
+ assertTrue(operation.updates().scope(Scope.compile).isEmpty());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ void testExplicitVersionCheckedDespiteBom()
+ throws Exception {
+ var server = createArtifactServer(
+ Map.of("bom1:1.0.0", bomPom("bom1", "1.0.0", "com.examplea1.4.0")),
+ Map.of("bom1", metadata("bom1", "1.0.0", "1.0.0"),
+ "a", metadata("a", "2.0.0", "1.0.0", "2.0.0")));
+ server.start();
+ try {
+ var operation = new UpdatesOperation()
+ .artifactRetriever(ArtifactRetriever.cachingInstance())
+ .repositories(List.of(new Repository("http://localhost:" + server.getAddress().getPort() + "/")));
+ operation.dependencies().scope(Scope.compile)
+ .include(new Bom("com.example", "bom1", new VersionNumber(1, 0, 0)))
+ .include(new Dependency("com.example", "a", new VersionNumber(1, 0, 0)));
+
+ operation.execute();
+
+ // the BOM is up to date and not reported
+ assertTrue(operation.updates().scope(Scope.compile).boms().isEmpty());
+
+ // the explicitly versioned dependency is still checked for
+ // updates, even though the BOM covers it
+ var dependencies = operation.updates().scope(Scope.compile);
+ assertEquals(1, dependencies.size());
+ assertEquals(new VersionNumber(2, 0, 0), dependencies.iterator().next().version());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ private static HttpServer createArtifactServer(Map poms, Map metadata)
+ throws IOException {
+ var server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+ server.createContext("/", exchange -> {
+ var segments = exchange.getRequestURI().getPath().split("/");
+ var filename = segments[segments.length - 1];
+ String content = null;
+ if (filename.endsWith(".pom") && segments.length >= 3) {
+ content = poms.get(segments[segments.length - 3] + ":" + segments[segments.length - 2]);
+ } else if (filename.equals("maven-metadata.xml") && segments.length >= 2) {
+ content = metadata.get(segments[segments.length - 2]);
+ }
+ if (content == null) {
+ exchange.sendResponseHeaders(404, -1);
+ } else {
+ var body = content.getBytes();
+ exchange.sendResponseHeaders(200, body.length);
+ exchange.getResponseBody().write(body);
+ }
+ exchange.close();
+ });
+ return server;
+ }
+
+ private static String bomPom(String artifact, String version, String managedDependencies) {
+ return """
+
+
+ 4.0.0
+ com.example
+ %s
+ %s
+ pom
+
+ %s
+
+ """.formatted(artifact, version, managedDependencies);
+ }
+
+ private static String metadata(String artifact, String latest, String... versions) {
+ var versions_xml = new StringBuilder();
+ for (var version : versions) {
+ versions_xml.append("").append(version).append("");
+ }
+ return """
+
+
+ com.example
+ %s
+
+ %s
+ %s
+ %s
+
+ """.formatted(artifact, latest, latest, versions_xml);
+ }
}