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); + } }