This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 669fe51c400a [camel-4.22.x] CAMEL-26344: camel-jbang - plugin add
selects the plugin release built for the Camel CLI (#28584)
669fe51c400a is described below
commit 669fe51c400a704ddeccafd1678a4bb220c5d39e
Author: Federico Mariani <[email protected]>
AuthorDate: Wed Oct 28 12:00:13 2026 +0100
[camel-4.22.x] CAMEL-26344: camel-jbang - plugin add selects the plugin
release built for the Camel CLI (#28584)
* CAMEL-26344: camel-tooling-maven - read the resolved POM of an artifact
Add MavenDownloader.resolveArtifactDescriptor, which reads the effective
POM of an artifact (direct dependencies with versions from parents and
imported BOMs, and the interpolated model properties) using the same
repositories, mirrors, settings and offline mode as resolveArtifacts.
Expose it through DependencyDownloader for the Camel CLI.
(cherry picked from commit 349a1bfad94d535303f567b3369f4cfef63ead54)
* CAMEL-26344: camel-jbang - plugin add selects the plugin release built
for the Camel CLI
camel plugin add <name> registered community plugins with Maven LATEST,
which can be a release built for another Camel version. It now reads the
POMs of the plugin releases, newest first (at most 10), and selects the
newest one whose supported Camel versions include the CLI version: the
camel.jbang.plugin.camel-versions property (a Maven version range), or
else the minor line of its camel-jbang-core dependency. The selected
release is downloaded before the configuration is saved, and stored as an
exact version together with its supported Camel versions.
When no release matches, or the releases cannot be read, it warns and
registers LATEST as before. An explicit --version or --gav is registered
as given, with a warning when the release is built for another Camel
version. Loading a plugin warns when its stored Camel versions do not
include the CLI version, such as after a CLI upgrade. Built-in plugins
are unchanged.
(cherry picked from commit 74edf5f96d06a6fc1abc01babb53a8dc559eefe4)
* CAMEL-26344: camel-jbang - document how plugin add selects the plugin
release
Document the selection of the plugin release built for the Camel CLI, the
camel.jbang.plugin.camel-versions property for plugin authors, the
inference from camel-jbang-core, the exact version stored, explicit
--version/--gav, and how to pin a matching release with older CLIs.
(cherry picked from commit bb5d93ea02db96704fdd0124cdb2b466a52b64dc)
* CAMEL-26344: camel-tooling-maven - list the available versions from all
the configured repositories
Add MavenDownloader.resolveAvailableVersions(groupId, artifactId,
extraRepositories, fresh), which merges the versions of all the
configured remote repositories (Maven Central, the repositories of the
active profiles in the Maven settings, and the configured repositories,
with mirrors applied) and skips a repository that cannot be reached.
With fresh, the remote repositories are checked again instead of using
the metadata downloaded before. Expose it through DependencyDownloader.
(cherry picked from commit 445943bb3602bd62765977fa1c14177e212d0fb3)
* CAMEL-26344: camel-jbang - plugin add checks the releases again and reads
one POM per release line
Address review comments:
- list the plugin releases fresh, from all the configured repositories
(including the repositories of the Maven settings.xml), so a new
release is found when plugin add runs again; the POM reads keep using
the local repository, as a release POM does not change
- read only the POM of the newest release of each release line of the
plugin (major.minor), at most 10 lines, so a release for an older
Camel line is still found when the plugin has many releases
- when the CLI is between the Camel versions of the releases read,
select the newest release for the nearest older Camel version with a
warning, instead of LATEST; keep LATEST when the CLI is newer or older
than all of them
- declare the maven-resolver dependencies used for the version ranges
(cherry picked from commit 1f08c3ef87b7bb295f0029576371536f183d3bf7)
* CAMEL-26344: camel-jbang - update the plugin version docs after review
Document which repositories the release list uses, the walk by release
line and the selection of the nearest older Camel version, describe how
to pin a release with older CLIs instead of listing releases, and add the
upgrade note to the 4.22.3 section of the 4.22 upgrade guide.
(cherry picked from commit e027d238d0c0b56e53d4878598a849ef4012e9a2)
* CAMEL-26344: camel-jbang - Keep the nearest older selection quiet on the
same Camel version, and read the next release of a line when a POM cannot be
read
---
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 14 +
.../modules/ROOT/pages/camel-jbang-projects.adoc | 86 +++++-
dsl/camel-jbang/camel-jbang-core/pom.xml | 11 +
.../dsl/jbang/core/commands/plugin/PluginAdd.java | 189 ++++++++++++-
.../dsl/jbang/core/common/PluginCompatibility.java | 222 +++++++++++++++
.../camel/dsl/jbang/core/common/PluginHelper.java | 33 +++
.../jbang/core/common/PluginVersionResolver.java | 241 +++++++++++++++++
.../jbang/core/commands/plugin/PluginAddTest.java | 271 ++++++++++++++++++-
.../jbang/core/common/FakePluginDownloader.java | 65 +++++
.../jbang/core/common/PluginCompatibilityTest.java | 152 +++++++++++
.../dsl/jbang/core/common/PluginHelperTest.java | 72 +++++
.../core/common/PluginVersionResolverTest.java | 299 +++++++++++++++++++++
.../camel/main/download/DependencyDownloader.java | 32 +++
.../main/download/MavenDependencyDownloader.java | 35 +++
tooling/camel-tooling-maven/pom.xml | 6 +
.../tooling/maven/MavenArtifactDescriptor.java | 44 +++
.../camel/tooling/maven/MavenDownloader.java | 33 +++
.../camel/tooling/maven/MavenDownloaderImpl.java | 223 ++++++++++++---
.../tooling/maven/MavenDownloaderImplTest.java | 272 +++++++++++++++++++
19 files changed, 2261 insertions(+), 39 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 07089ed61ee4..31a92c4e78eb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -56,6 +56,20 @@ The WordPress client no longer logs every HTTP request and
response it exchanges
level (the `org.apache.cxf.services` loggers). The component no longer depends
on `cxf-rt-features-logging`, and the
`camel-wordpress` JAR no longer contains a `log4j.properties` file.
+=== camel-jbang - camel plugin add selects the plugin release built for the CLI
+
+`camel plugin add <name>` no longer registers a community plugin (such as
`forage` or `kit`) with the Maven `LATEST`
+version when no version is given. It selects the newest release of the plugin
built for the version of the CLI, from
+the `camel.jbang.plugin.camel-versions` property in the POM of the plugin, or
else from the version of its
+`camel-jbang-core` dependency. The exact version is stored in
`~/.camel-jbang-plugins.json`, so the plugin no longer
+moves to newer releases on its own: run `camel plugin add <name>` again to
select the newest matching release.
+When the CLI is between the Camel versions of the releases, it selects the
newest release for the nearest older Camel
+version, with a warning. When the CLI is newer than all the releases, or the
releases cannot be read, it warns and uses
+`LATEST` as before.
+
+`--version` and `--gav` are used as given, with a warning when the release is
built for another Camel version. Plugins
+registered before are not changed. See
xref:camel-jbang-projects.adoc#_plugin_versions[Plugin versions].
+
== Upgrading from 4.22.1 to 4.22.2
=== camel-management - Route group exchange counters count once per entry into
the group again
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
index b924553da6c9..d791c714c823 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
@@ -233,11 +233,80 @@ Add, list installed, and remove plugins:
[source,bash]
----
camel plugin add generate
-camel plugin add forage --version=1.2.3
+camel plugin add forage
camel plugin get
camel plugin delete <plugin-name>
----
+=== Plugin versions
+
+The plugins of the Apache Camel project always use the version of the CLI.
+
+A community plugin, such as `forage` or `kit`, is released on its own, and
each release is built for a Camel version.
+`camel plugin add <name>` selects the newest release of the plugin built for
the version of the installed CLI, and prints it:
+
+[source,bash]
+----
+$ camel plugin add forage
+Selected forage 1.6.2 for Camel CLI 4.22.2 (built for Camel 4.22.2)
+Plugin forage added
+----
+
+To select the release, the CLI lists the releases of the plugin from the Maven
repositories it is configured with:
+Maven Central, the repositories of the active profiles in the Maven
`settings.xml` (with its mirrors applied), and the
+repositories given with `--repos` (or by the catalog of known plugins). The
list is always checked again in these
+repositories, so a new release is found. Releases with a qualifier, such as
`-SNAPSHOT` or `-RC1`, are not used.
+
+The releases are grouped by release line of the plugin (its major and minor
version: `1.6.0`, `1.6.1` and `1.6.2` are
+the line `1.6`), and the CLI reads the POM of the newest release of each line,
newest line first (at most 10 lines). The
+Camel versions a release supports are:
+
+. the Maven version range in the property `camel.jbang.plugin.camel-versions`
of its POM, when the plugin declares it
+(see <<Building custom plugins>>)
+. otherwise the minor version of Camel of its
`org.apache.camel:camel-jbang-core` dependency: a release built with
+Camel 4.22.2 supports Camel 4.22.x (the range `[4.22.0,4.23.0)`)
+
+A SNAPSHOT build of the CLI is compared without `-SNAPSHOT`. The
`--camel-version` option of the commands is not used for
+this: it is the version of the installed CLI that counts.
+
+`camel plugin add` downloads the selected release before it saves the
configuration, and stores the exact version in
+`~/.camel-jbang-plugins.json`, together with the Camel versions it supports.
The plugin therefore stays at that version:
+run `camel plugin add <name>` again to select the newest matching release, for
example after a new patch release of the
+plugin. When the CLI is upgraded to a Camel version that the stored release
does not support, the CLI warns when the
+plugin is loaded, and tells to run `camel plugin add <name>` again. Plugins
added with an older CLI are not checked.
+
+The CLI selects the newest release of the first line that supports its
version. When no release read supports it:
+
+* if the CLI is newer than the Camel versions of all the releases (for
example, the plugin has no release for a new
+Camel version yet), `camel plugin add` warns and uses the newest release
(Maven `LATEST`), as older versions of the CLI
+do
+* if the CLI is between the Camel versions of the releases, it selects the
newest release for the nearest older Camel
+version, and warns
+* if the CLI is older than the Camel versions of all the releases read, or no
release tells which Camel versions it
+supports, it warns and uses `LATEST`
+
+The warnings list the newest release per Camel version found. When the
releases cannot be read (for example, when
+offline), `camel plugin add` warns that it could not check the compatibility,
and uses `LATEST`.
+
+To use a specific release, give its version, or the Maven coordinates of the
plugin. It is used as given; the CLI only
+warns when the release is known to be built for another Camel version:
+
+[source,bash]
+----
+camel plugin add forage --version 1.6.2
+camel plugin add forage --gav io.kaoto.forage:camel-jbang-plugin-forage:1.6.2
+----
+
+Older versions of the CLI, such as Camel 4.22.0 to 4.22.2, do not select a
release, and use the newest one. With them,
+give the release the plugin documents for your Camel version with `--version`,
or with `--gav` before Camel 4.21, where
+the CLI does not know the Maven `groupId` of community plugins yet (and
`--version` alone looks for the plugin in the
+`org.apache.camel` group). For example, on Camel 4.22.0 to 4.22.2, Forage
1.6.2 is built for Camel 4.22:
+
+[source,bash]
+----
+camel plugin add forage --version 1.6.2
+----
+
=== Building custom plugins
It is possible to build custom Camel CLI plugins. We suggest to take a look at
one of the https://github.com/apache/camel/tree/main/dsl/camel-jbang[existing
plugins],
@@ -246,6 +315,21 @@ to see how that is done.
IMPORTANT: The name of the plugin Maven **artifactId** must start with
`camel-jbang-plugin-`. For example if the plugin is named `cheese`, then
the maven artifact must be named `camel-jbang-plugin-cheese`.
+A plugin should declare the Camel versions each release supports, as a Maven
version range in the property
+`camel.jbang.plugin.camel-versions` of the POM of the plugin, so `camel plugin
add` selects the right release
+(see <<Plugin versions>>):
+
+[source,xml]
+----
+<properties>
+
<camel.jbang.plugin.camel-versions>[4.23.0,4.24.0)</camel.jbang.plugin.camel-versions>
+</properties>
+----
+
+Keep the value a literal range, updated together with the Camel version the
plugin is built with. A value that is not a
+version range (such as a single version) is ignored. Without the property, a
release is taken to support the minor
+version of Camel of its `org.apache.camel:camel-jbang-core` dependency.
+
== Generate JSON schema plugin
Generate JSON Schema definitions from Java objects for use with visual
development tools (Kaoto, Karavan):
diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml
b/dsl/camel-jbang/camel-jbang-core/pom.xml
index 5a025e9d419b..445f2997f39d 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -64,6 +64,17 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-kamelet-main</artifactId>
</dependency>
+ <!-- Maven version ranges, to select the plugin release built for the
Camel CLI -->
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-api</artifactId>
+ <version>${maven-resolver-version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-util</artifactId>
+ <version>${maven-resolver-version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-resourceresolver-github</artifactId>
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
index 4b639c653e24..30b2245cdc2e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java
@@ -21,8 +21,21 @@ import java.util.Optional;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.Plugin;
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility;
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Compat;
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Source;
import org.apache.camel.dsl.jbang.core.common.PluginHelper;
import org.apache.camel.dsl.jbang.core.common.PluginType;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.NoMatch;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Older;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Result;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Selected;
+import
org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Unavailable;
+import org.apache.camel.main.download.DependencyDownloader;
+import org.apache.camel.main.download.MavenDependencyDownloader;
+import org.apache.camel.tooling.maven.MavenGav;
import org.apache.camel.util.json.JsonObject;
import picocli.CommandLine;
@@ -72,6 +85,9 @@ public class PluginAdd extends PluginBaseCommand {
description = "Maven group and artifact coordinates.")
String gav;
+ @CommandLine.Spec
+ CommandLine.Model.CommandSpec commandSpec;
+
public PluginAdd(CamelJBangMain main) {
super(main);
}
@@ -137,13 +153,21 @@ public class PluginAdd extends PluginBaseCommand {
plugin.put("description",
description != null ? description : "Plugin %s called with
command %s".formatted(name, command));
+ boolean explicit = gav != null || isVersionGiven();
if (gav == null && (groupId != null && artifactId != null)) {
if (version == null) {
if ("org.apache.camel".equals(groupId)) {
CamelCatalog catalog = new DefaultCamelCatalog();
version = catalog.getCatalogVersion();
} else {
- version = "LATEST";
+ // a community plugin: select the newest release built for
this Camel CLI
+ if (!selectVersion(plugin)) {
+ // the selected release cannot be downloaded
+ return 1;
+ }
+ if (version == null) {
+ version = "LATEST";
+ }
}
}
@@ -152,6 +176,9 @@ public class PluginAdd extends PluginBaseCommand {
if (gav != null) {
plugin.put("dependency", gav);
+ if (explicit) {
+ warnOnMismatch(gav);
+ }
}
if (repositories != null) {
plugin.put("repos", repositories);
@@ -164,4 +191,164 @@ public class PluginAdd extends PluginBaseCommand {
return 0;
}
+ /**
+ * Selects the newest release of a community plugin built for this Camel
CLI, and downloads it, so a release that
+ * cannot be used never gets into the configuration. Sets {@link #version}
to the selected release, or leaves it
+ * null to use LATEST when no release can be selected.
+ *
+ * @return false when the selected release cannot be downloaded
+ */
+ private boolean selectVersion(JsonObject plugin) throws Exception {
+ String cli = cliVersion();
+ Result result;
+ DependencyDownloader downloader = newDownloader();
+ try {
+ result = new PluginVersionResolver(downloader).resolve(groupId,
artifactId, cli);
+ } finally {
+ downloader.stop();
+ downloader.close();
+ }
+
+ if (result instanceof Selected selected) {
+ if (!useRelease(plugin, selected.version(), selected.compat())) {
+ return false;
+ }
+ printer().printf("Selected %s %s for Camel CLI %s (%s)%n", name,
version, cli, selected.compat().describe());
+ } else if (result instanceof Older older) {
+ if (!useRelease(plugin, older.version(), older.compat())) {
+ return false;
+ }
+ // warned here, so the load-time check stays quiet until the CLI
moves to another Camel version
+ plugin.put("selectedForCamel", PluginCompatibility.minorLine(cli));
+ printer().println(
+ "WARN: No release of %s is built for Camel CLI %s.
Selected %s %s, the newest release for an older Camel version (%s)."
+ .formatted(name, cli, name, version,
older.compat().describe()));
+ } else if (result instanceof NoMatch noMatch) {
+ StringBuilder sb = new StringBuilder();
+ switch (noMatch.reason()) {
+ case CLI_NEWER -> sb.append("No release of %s is built for
Camel CLI %s yet. The newest, %s %s, is %s."
+ .formatted(name, cli, name, noMatch.newestVersion(),
noMatch.newest().describe()));
+ case CLI_OLDER -> sb.append("No release of %s is built for
Camel CLI %s or an older Camel version."
+ .formatted(name, cli));
+ default -> sb.append("No release of %s tells which Camel
versions it supports.".formatted(name));
+ }
+ sb.append(" Using LATEST.");
+ if (!noMatch.newestPerLine().isEmpty()) {
+ sb.append(System.lineSeparator()).append("Newest release per
Camel version:");
+ noMatch.newestPerLine().forEach((line, v) ->
sb.append(System.lineSeparator())
+ .append(" Camel %s: %s %s".formatted(line, name, v)));
+ sb.append(System.lineSeparator())
+ .append("To use one of them: camel plugin add %s
--version <version>".formatted(name));
+ }
+ printer().println("WARN: " + sb);
+ } else if (result instanceof Unavailable unavailable) {
+ printer().println("WARN: Could not check plugin compatibility
(%s). Using LATEST.".formatted(unavailable.reason()));
+ }
+ return true;
+ }
+
+ /**
+ * Downloads the release, and stores the Camel versions it supports in the
plugin entry.
+ *
+ * @return false when the release cannot be downloaded
+ */
+ private boolean useRelease(JsonObject plugin, String release, Compat
compat) {
+ version = release;
+ String releaseGav = "%s:%s:%s".formatted(groupId, artifactId, version);
+ Optional<Plugin> resolved;
+ try {
+ resolved = downloadPlugin(releaseGav);
+ } catch (Exception e) {
+ printer().printErr("Cannot download " + releaseGav, e);
+ return false;
+ }
+ if (resolved.isEmpty()) {
+ printer().printErr("Cannot download " + releaseGav);
+ return false;
+ }
+ plugin.put("camelVersions", compat.range());
+ plugin.put("camelVersionsSource", compat.source().label());
+ return true;
+ }
+
+ /**
+ * Warns when a release asked for with --version or --gav is known to be
built for another Camel version. Best
+ * effort: says nothing when the POM cannot be read, and never fails the
command.
+ */
+ private void warnOnMismatch(String dependency) {
+ MavenGav mavenGav;
+ try {
+ mavenGav = MavenGav.parseGav(dependency);
+ } catch (Exception e) {
+ return;
+ }
+ String g = mavenGav.getGroupId();
+ String v = mavenGav.getVersion();
+ // the Camel plugins are released with Camel, and LATEST or a range is
not a release
+ if (g == null || "org.apache.camel".equals(g) || v == null ||
v.isEmpty() || !Character.isDigit(v.charAt(0))) {
+ return;
+ }
+ String cli = cliVersion();
+ DependencyDownloader downloader = null;
+ try {
+ downloader = newDownloader();
+ Compat compat = PluginCompatibility.of(
+ downloader.resolveArtifactDescriptor(g,
mavenGav.getArtifactId(), v, null));
+ if (compat.source() != Source.UNKNOWN &&
!PluginCompatibility.accepts(compat.range(), cli)) {
+ printer().println("WARN: %s %s is built for Camel %s; this CLI
is %s".formatted(name, v, compat.range(), cli));
+ }
+ } catch (Exception e) {
+ // best effort
+ } finally {
+ if (downloader != null) {
+ try {
+ downloader.stop();
+ downloader.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Whether --version was given on the command line (its default comes from
a {@code camel-version} system property
+ * or environment variable).
+ */
+ private boolean isVersionGiven() {
+ CommandLine.ParseResult result = commandSpec != null ?
commandSpec.commandLine().getParseResult() : null;
+ if (result != null) {
+ return result.hasMatchedOption("--version");
+ }
+ // without picocli (such as in tests) the field is only set when asked
for
+ return version != null;
+ }
+
+ /**
+ * The version of this Camel CLI.
+ */
+ String cliVersion() {
+ return new DefaultCamelCatalog().getCatalogVersion();
+ }
+
+ /**
+ * A started downloader to read the releases and POMs of a plugin with.
+ */
+ DependencyDownloader newDownloader() {
+ DependencyDownloader downloader = new MavenDependencyDownloader();
+ if (repositories != null && !repositories.isBlank()) {
+ downloader.setRepositories(repositories);
+ }
+ downloader.setPreferLocal(true);
+ downloader.start();
+ return downloader;
+ }
+
+ /**
+ * Downloads the plugin, as when it is used.
+ */
+ Optional<Plugin> downloadPlugin(String dependency) {
+ return PluginHelper.getPlugin(command, cliVersion(), dependency,
repositories, printer());
+ }
+
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginCompatibility.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginCompatibility.java
new file mode 100644
index 000000000000..c79444170b36
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginCompatibility.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.tooling.maven.MavenArtifactDescriptor;
+import org.apache.camel.tooling.maven.MavenGav;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.eclipse.aether.version.VersionRange;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The Camel versions a release of a plugin supports, from its resolved POM:
+ * <ol>
+ * <li>the property {@value #CAMEL_VERSIONS_PROPERTY}, a Maven version range
such as {@code [4.23.0,4.24.0)}
+ * (declared)</li>
+ * <li>otherwise the version of its {@code org.apache.camel:camel-jbang-core}
dependency, as its minor line:
+ * {@code 4.22.2} supports {@code [4.22.0,4.23.0)} (inferred)</li>
+ * <li>otherwise nothing is known (unknown)</li>
+ * </ol>
+ */
+public final class PluginCompatibility {
+
+ /**
+ * The property a plugin declares in its POM with the Camel versions it
supports, as a Maven version range.
+ */
+ public static final String CAMEL_VERSIONS_PROPERTY =
"camel.jbang.plugin.camel-versions";
+
+ private static final Logger LOG =
LoggerFactory.getLogger(PluginCompatibility.class);
+
+ private static final GenericVersionScheme VERSION_SCHEME = new
GenericVersionScheme();
+ private static final Pattern MINOR_LINE =
Pattern.compile("^(\\d{1,9})\\.(\\d{1,9})(?:\\.\\d+)*(?:[-.].*)?$");
+
+ public enum Source {
+ DECLARED,
+ INFERRED,
+ UNKNOWN;
+
+ /**
+ * The name stored in the plugin configuration, such as {@code
declared}.
+ */
+ public String label() {
+ return name().toLowerCase();
+ }
+ }
+
+ /**
+ * What is known about a plugin release.
+ *
+ * @param source where the range comes from
+ * @param range the supported Camel versions as a Maven version range,
or null when unknown
+ * @param builtFor the version of camel-jbang-core the release is built
with, or null
+ */
+ public record Compat(Source source, String range, String builtFor) {
+
+ /**
+ * Describes the Camel versions of the release, such as {@code built
for Camel 4.22.2}.
+ */
+ public String describe() {
+ if (builtFor != null) {
+ return "built for Camel " + builtFor;
+ }
+ return range != null ? "built for Camel " + range : "built for an
unknown Camel version";
+ }
+ }
+
+ private PluginCompatibility() {
+ }
+
+ /**
+ * What the resolved POM of a plugin release tells about the Camel
versions it supports.
+ */
+ public static Compat of(MavenArtifactDescriptor descriptor) {
+ MavenGav core = descriptor.findDependency("org.apache.camel",
"camel-jbang-core");
+ String builtFor = core != null ? core.getVersion() : null;
+
+ String declared = descriptor.properties().get(CAMEL_VERSIONS_PROPERTY);
+ if (declared != null && !declared.isBlank()) {
+ String range = declared.trim();
+ if (isValidRange(range)) {
+ return new Compat(Source.DECLARED, range, builtFor);
+ }
+ LOG.debug("Ignoring invalid {} in {}: {}",
CAMEL_VERSIONS_PROPERTY, descriptor.gav(), declared);
+ return new Compat(Source.UNKNOWN, null, builtFor);
+ }
+
+ String inferred = minorLineRange(builtFor);
+ if (inferred != null) {
+ return new Compat(Source.INFERRED, inferred, builtFor);
+ }
+ return new Compat(Source.UNKNOWN, null, builtFor);
+ }
+
+ /**
+ * Whether the range includes the given Camel CLI version. A SNAPSHOT
version is compared without -SNAPSHOT.
+ *
+ * @return true if included, false if not, or when the range or the
version is invalid
+ */
+ public static boolean accepts(String range, String cliVersion) {
+ Version cli = parseCliVersion(cliVersion);
+ if (range == null || cli == null) {
+ return false;
+ }
+ try {
+ VersionConstraint constraint =
VERSION_SCHEME.parseVersionConstraint(range);
+ return constraint.containsVersion(cli);
+ } catch (InvalidVersionSpecificationException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Whether the whole range is below the given Camel CLI version, that is,
the range is for older Camel versions
+ * only. A SNAPSHOT version is compared without -SNAPSHOT.
+ *
+ * @return true if the range has an upper bound below the version, false
if not, or when the range or the version is
+ * invalid
+ */
+ public static boolean isOlder(String range, String cliVersion) {
+ VersionRange.Bound upper = upperBound(range);
+ Version cli = parseCliVersion(cliVersion);
+ if (upper == null || cli == null) {
+ return false;
+ }
+ int cmp = cli.compareTo(upper.getVersion());
+ return cmp > 0 || cmp == 0 && !upper.isInclusive();
+ }
+
+ /**
+ * Compares two ranges by their upper bound: a range for older Camel
versions comes first. A range without upper
+ * bound comes last.
+ */
+ public static int compareUpperBounds(String range1, String range2) {
+ VersionRange.Bound b1 = upperBound(range1);
+ VersionRange.Bound b2 = upperBound(range2);
+ if (b1 == null || b2 == null) {
+ return b1 == null ? (b2 == null ? 0 : 1) : -1;
+ }
+ int cmp = b1.getVersion().compareTo(b2.getVersion());
+ if (cmp == 0 && b1.isInclusive() != b2.isInclusive()) {
+ return b1.isInclusive() ? 1 : -1;
+ }
+ return cmp;
+ }
+
+ private static VersionRange.Bound upperBound(String range) {
+ if (range == null) {
+ return null;
+ }
+ try {
+ VersionConstraint constraint =
VERSION_SCHEME.parseVersionConstraint(range);
+ VersionRange r = constraint.getRange();
+ return r != null ? r.getUpperBound() : null;
+ } catch (InvalidVersionSpecificationException e) {
+ return null;
+ }
+ }
+
+ private static Version parseCliVersion(String cliVersion) {
+ if (cliVersion == null) {
+ return null;
+ }
+ String v = cliVersion.endsWith("-SNAPSHOT")
+ ? cliVersion.substring(0, cliVersion.length() -
"-SNAPSHOT".length()) : cliVersion;
+ try {
+ return VERSION_SCHEME.parseVersion(v);
+ } catch (InvalidVersionSpecificationException e) {
+ return null;
+ }
+ }
+
+ /**
+ * The minor line of a version, such as {@code 4.22} for {@code 4.22.2},
or null.
+ */
+ public static String minorLine(String version) {
+ Matcher m = version != null ? MINOR_LINE.matcher(version) : null;
+ return m != null && m.matches() ? m.group(1) + "." + m.group(2) : null;
+ }
+
+ /**
+ * The range of the minor line of a version, such as {@code
[4.22.0,4.23.0)} for {@code 4.22.2}, or null.
+ */
+ static String minorLineRange(String version) {
+ Matcher m = version != null ? MINOR_LINE.matcher(version) : null;
+ if (m == null || !m.matches()) {
+ return null;
+ }
+ String major = m.group(1);
+ int minor = Integer.parseInt(m.group(2));
+ return "[" + major + "." + minor + ".0," + major + "." + (minor + 1) +
".0)";
+ }
+
+ private static boolean isValidRange(String range) {
+ try {
+ VersionConstraint constraint =
VERSION_SCHEME.parseVersionConstraint(range);
+ // a single version is a recommendation in Maven, not a range
+ return constraint.getRange() != null;
+ } catch (InvalidVersionSpecificationException e) {
+ return false;
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
index 5896dc18ce98..0f69270f631c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
@@ -34,6 +34,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@@ -72,6 +73,9 @@ public final class PluginHelper {
*/
private static final Set<String> PLUGIN_CONSUMING_BUILTINS =
Set.of("shell", "run", "export", "cmd");
+ // the plugins warned about by compatibilityCheck, to warn once
+ private static final Set<String> COMPATIBILITY_WARNED =
ConcurrentHashMap.newKeySet();
+
private static final FactoryFinder FACTORY_FINDER
= new DefaultFactoryFinder(new DefaultClassResolver(),
FactoryFinder.DEFAULT_PATH + "camel-jbang-plugin/");
@@ -228,6 +232,10 @@ public final class PluginHelper {
if (!version.isBlank() && !firstVersion.isBlank()) {
versionCheck(main, version, firstVersion, command);
}
+ // check if the plugin release is built for this camel version
(only stored data, no download)
+ if (!version.isBlank()) {
+ compatibilityCheck(main.getOut(), version, name,
properties);
+ }
ResolveResult res = resolvePlugin(properties, command,
version, gav, repos, main.getOut());
if (res.plugin().isPresent()) {
@@ -495,6 +503,31 @@ public final class PluginHelper {
}
}
+ /**
+ * Warns, once per plugin, when the stored Camel versions of a plugin
release (see {@link PluginCompatibility}) do
+ * not include the Camel CLI version, such as after upgrading the CLI.
Plugins registered without them are not
+ * checked.
+ */
+ static void compatibilityCheck(Printer printer, String version, String
name, JsonObject properties) {
+ String range = asString(properties.get("camelVersions"));
+ if (range == null || range.isBlank() ||
PluginCompatibility.accepts(range, version)) {
+ return;
+ }
+ // the nearest older release, selected for this Camel version as no
release is built for it
+ String selectedFor = asString(properties.get("selectedForCamel"));
+ if (selectedFor != null &&
selectedFor.equals(PluginCompatibility.minorLine(version))) {
+ return;
+ }
+ String gav = asString(properties.get("dependency"));
+ MavenGav mavenGav = dependencyAsMavenGav(gav);
+ String pluginVersion = mavenGav != null && mavenGav.getVersion() !=
null ? " " + mavenGav.getVersion() : "";
+ if (COMPATIBILITY_WARNED.add(name + ":" + gav + ":" + range)) {
+ printer.println(
+ "WARN: %s%s is built for Camel %s but this CLI is %s. Run
'camel plugin add %s' to select a matching release."
+ .formatted(name, pluginVersion, range, version,
name));
+ }
+ }
+
private static DownloadResult downloadPlugin(
String command, String camelVersion, String version, String group,
String repos, Printer printer) {
DependencyDownloader downloader = new MavenDependencyDownloader();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginVersionResolver.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginVersionResolver.java
new file mode 100644
index 000000000000..2acd7e6fa1a3
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginVersionResolver.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.regex.Pattern;
+
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Compat;
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Source;
+import org.apache.camel.main.download.DependencyDownloader;
+import org.apache.camel.tooling.maven.MavenArtifactDescriptor;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Selects the newest release of a plugin that supports the version of the
Camel CLI, from the resolved POMs of its
+ * releases (see {@link PluginCompatibility}).
+ * <p>
+ * The releases are grouped by release line of the plugin (its major and minor
version, such as {@code 1.6} for
+ * {@code 1.6.0}, {@code 1.6.1} and {@code 1.6.2}), and only the POM of the
newest release of each line is read, newest
+ * line first, so a release for an older Camel version is found with few reads.
+ */
+public final class PluginVersionResolver {
+
+ /**
+ * The most POMs of the plugin that are read, from the newest release of
each release line, newest line first. When
+ * the POM of the newest release of a line cannot be read, the next
release of the line is read.
+ */
+ static final int MAX_RELEASE_LINES = 10;
+
+ private static final Logger LOG =
LoggerFactory.getLogger(PluginVersionResolver.class);
+
+ private static final GenericVersionScheme VERSION_SCHEME = new
GenericVersionScheme();
+ // a release without qualifier, such as 1.6.2 (no SNAPSHOT, RC, or other
qualifier)
+ private static final Pattern RELEASE = Pattern.compile("^\\d+(\\.\\d+)*$");
+
+ public sealed interface Result permits Selected, Older, NoMatch,
Unavailable {
+ }
+
+ /**
+ * The newest release that supports the Camel CLI.
+ */
+ public record Selected(String version, Compat compat) implements Result {
+ }
+
+ /**
+ * No release read supports the Camel CLI, which is between the Camel
versions of the releases read: the newest
+ * release for the nearest older Camel version.
+ *
+ * @param version the release
+ * @param compat what is known about the release
+ * @param newestPerLine the newest release per Camel line (such as {@code
4.22 -> 1.6.2}), newest release first,
+ * from the releases read
+ */
+ public record Older(String version, Compat compat, Map<String, String>
newestPerLine) implements Result {
+ }
+
+ /**
+ * Why no release can be selected.
+ */
+ public enum Reason {
+ /**
+ * The Camel CLI is newer than the Camel versions of all the releases
read.
+ */
+ CLI_NEWER,
+ /**
+ * The Camel CLI is older than the Camel versions of all the releases
read.
+ */
+ CLI_OLDER,
+ /**
+ * No release read tells which Camel versions it supports.
+ */
+ UNKNOWN
+ }
+
+ /**
+ * No release can be selected.
+ *
+ * @param reason why
+ * @param newest what is known about the newest release
+ * @param newestVersion the newest release
+ * @param newestPerLine the newest release per Camel line (such as {@code
4.22 -> 1.6.2}), newest release first,
+ * from the releases read
+ */
+ public record NoMatch(Reason reason, Compat newest, String newestVersion,
Map<String, String> newestPerLine)
+ implements
+ Result {
+ }
+
+ /**
+ * The releases or their POMs could not be read, such as when offline.
+ */
+ public record Unavailable(String reason) implements Result {
+ }
+
+ private record Read(String version, Compat compat) {
+ }
+
+ private final DependencyDownloader downloader;
+
+ /**
+ * @param downloader a started downloader, configured with the
repositories to read the releases and POMs from
+ */
+ public PluginVersionResolver(DependencyDownloader downloader) {
+ this.downloader = downloader;
+ }
+
+ /**
+ * Selects the newest release of the plugin that supports the Camel CLI.
+ *
+ * @param groupId the groupId of the plugin
+ * @param artifactId the artifactId of the plugin
+ * @param cliVersion the version of the Camel CLI
+ */
+ public Result resolve(String groupId, String artifactId, String
cliVersion) {
+ List<String> versions;
+ try {
+ // the releases change, so check the repositories again (the POM
of a release does not change)
+ versions = downloader.resolveAvailableVersions(groupId,
artifactId, true);
+ } catch (Exception e) {
+ LOG.debug("Cannot list versions of {}:{}: {}", groupId,
artifactId, e.getMessage(), e);
+ return new Unavailable(
+ e.getMessage() != null
+ ? e.getMessage()
+ : "cannot list the releases of " + groupId + ":" +
artifactId);
+ }
+
+ // the releases of each release line, newest line and newest release
first
+ Map<String, List<String>> lines = new LinkedHashMap<>();
+ for (String version : newestFirst(versions)) {
+ lines.computeIfAbsent(releaseLine(version), l -> new
ArrayList<>()).add(version);
+ }
+ if (lines.isEmpty()) {
+ return new Unavailable("no release of " + groupId + ":" +
artifactId + " found");
+ }
+
+ String failure = null;
+ List<Read> reads = new ArrayList<>();
+ Map<String, String> newestPerLine = new LinkedHashMap<>();
+ for (List<String> releases : lines.values()) {
+ String version = null;
+ Compat compat = null;
+ // the newest release of the line, or the next one when its POM
cannot be read
+ for (String release : releases) {
+ if (reads.size() >= MAX_RELEASE_LINES) {
+ break;
+ }
+ try {
+ MavenArtifactDescriptor descriptor
+ = downloader.resolveArtifactDescriptor(groupId,
artifactId, release, null);
+ version = release;
+ compat = PluginCompatibility.of(descriptor);
+ break;
+ } catch (Exception e) {
+ LOG.debug("Cannot read the POM of {}:{}:{}: {}", groupId,
artifactId, release, e.getMessage(), e);
+ failure = e.getMessage();
+ // counts as a read, so an unreachable repository does not
read every release
+ reads.add(new Read(release, null));
+ }
+ }
+ if (compat == null) {
+ continue;
+ }
+ reads.add(new Read(version, compat));
+ if (compat.source() != Source.UNKNOWN &&
PluginCompatibility.accepts(compat.range(), cliVersion)) {
+ return new Selected(version, compat);
+ }
+ String line = compat.builtFor() != null ?
PluginCompatibility.minorLine(compat.builtFor()) : compat.range();
+ if (line != null) {
+ newestPerLine.putIfAbsent(line, version);
+ }
+ }
+
+ List<Read> read = reads.stream().filter(r -> r.compat() !=
null).toList();
+ if (read.isEmpty()) {
+ return new Unavailable(failure != null ? failure : "cannot read
the POMs of " + groupId + ":" + artifactId);
+ }
+ Read newest = read.get(0);
+ List<Read> known = read.stream().filter(r -> r.compat().source() !=
Source.UNKNOWN).toList();
+ if (known.isEmpty()) {
+ return new NoMatch(Reason.UNKNOWN, newest.compat(),
newest.version(), newestPerLine);
+ }
+ List<Read> older = known.stream().filter(r ->
PluginCompatibility.isOlder(r.compat().range(), cliVersion)).toList();
+ if (older.isEmpty()) {
+ return new NoMatch(Reason.CLI_OLDER, newest.compat(),
newest.version(), newestPerLine);
+ }
+ if (older.size() == known.size()) {
+ return new NoMatch(Reason.CLI_NEWER, newest.compat(),
newest.version(), newestPerLine);
+ }
+ // between Camel versions: the newest release for the nearest older one
+ Read nearest = older.get(0);
+ for (Read r : older) {
+ if (PluginCompatibility.compareUpperBounds(r.compat().range(),
nearest.compat().range()) > 0) {
+ nearest = r;
+ }
+ }
+ return new Older(nearest.version(), nearest.compat(), newestPerLine);
+ }
+
+ // the release line of a version: its major and minor version, such as 1.6
for 1.6.2
+ static String releaseLine(String version) {
+ String[] parts = version.split("\\.");
+ return parts.length >= 2 ? parts[0] + "." + parts[1] : version;
+ }
+
+ // the releases without qualifier, newest first
+ static List<String> newestFirst(List<String> versions) {
+ TreeMap<Version, String> sorted = new TreeMap<>();
+ for (String v : versions) {
+ if (v != null && RELEASE.matcher(v).matches()) {
+ try {
+ sorted.putIfAbsent(VERSION_SCHEME.parseVersion(v), v);
+ } catch (InvalidVersionSpecificationException e) {
+ // not a version
+ }
+ }
+ }
+ return new ArrayList<>(sorted.descendingMap().values());
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
index a7c43a2d7c76..4ec362861989 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAddTest.java
@@ -16,11 +16,19 @@
*/
package org.apache.camel.dsl.jbang.core.commands.plugin;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CachedFakePlugin;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.FakePluginDownloader;
+import org.apache.camel.dsl.jbang.core.common.Plugin;
import org.apache.camel.dsl.jbang.core.common.PluginHelper;
import org.apache.camel.dsl.jbang.core.common.PluginType;
+import org.apache.camel.main.download.DependencyDownloader;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -28,17 +36,20 @@ import org.junit.jupiter.api.Test;
class PluginAddTest extends CamelCommandBaseTestSupport {
+ private FakePluginDownloader downloader;
+
@BeforeEach
public void setup() throws Exception {
super.setup();
CommandLineHelper.useHomeDir("target");
PluginHelper.createPluginConfig();
+ downloader = new FakePluginDownloader();
}
@Test
public void shouldAddDefaultPlugin() throws Exception {
- PluginAdd command = new PluginAdd(new
CamelJBangMain().withPrinter(printer));
+ TestPluginAdd command = newCommand();
command.name = "kubernetes";
command.doCall();
@@ -52,7 +63,7 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
@Test
public void shouldResolveKitDependencyFromKnownPlugins() throws Exception {
- PluginAdd command = new PluginAdd(new
CamelJBangMain().withPrinter(printer));
+ TestPluginAdd command = newCommand();
command.name = "kit";
command.version = "0.4.0";
command.doCall();
@@ -66,7 +77,7 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
@Test
public void shouldAddPlugin() throws Exception {
- PluginAdd command = new PluginAdd(new
CamelJBangMain().withPrinter(printer));
+ TestPluginAdd command = newCommand();
command.name = "foo-plugin";
command.command = "foo";
command.description = "Some plugin";
@@ -82,7 +93,7 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
@Test
public void shouldGenerateProperties() throws Exception {
- PluginAdd command = new PluginAdd(new
CamelJBangMain().withPrinter(printer));
+ TestPluginAdd command = newCommand();
command.name = "foo";
command.doCall();
@@ -95,7 +106,7 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
@Test
public void shouldUseMavenGAV() throws Exception {
- PluginAdd command = new PluginAdd(new
CamelJBangMain().withPrinter(printer));
+ TestPluginAdd command = newCommand();
command.name = "foo-plugin";
command.command = "foo";
command.gav = "org.apache.camel:foo-plugin:1.0.0";
@@ -110,7 +121,7 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
@Test
public void shouldUseArtifactIdAndVersion() throws Exception {
- PluginAdd command = new PluginAdd(new
CamelJBangMain().withPrinter(printer));
+ TestPluginAdd command = newCommand();
command.name = "foo-plugin";
command.command = "foo";
command.groupId = "org.foo";
@@ -126,4 +137,252 @@ class PluginAddTest extends CamelCommandBaseTestSupport {
PluginHelper.getOrCreatePluginConfig().toJson());
}
+ @Test
+ public void shouldNotResolveVersionOfDefaultPlugin() throws Exception {
+ TestPluginAdd command = newCommand();
+ command.name = "kubernetes";
+ command.doCall();
+
+ Assertions.assertTrue(downloader.descriptorsRead.isEmpty());
+ Assertions.assertTrue(command.downloaded.isEmpty());
+ }
+
+ @Test
+ public void shouldSelectReleaseForCliVersion() throws Exception {
+ downloader.release("2.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("1.6.2", "4.22.2", null)
+ .release("1.6.1", "4.22.1", null);
+
+ TestPluginAdd command = newFixtureCommand();
+ Assertions.assertEquals(0, command.doCall());
+
+ Assertions.assertEquals("Selected fixture 1.6.2 for Camel CLI 4.22.0
(built for Camel 4.22.2)"
+ + System.lineSeparator() + "Plugin fixture
added",
+ printer.getOutput());
+
Assertions.assertEquals(List.of("org.example.plugins:camel-jbang-plugin-fixture:1.6.2"),
command.downloaded);
+
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:1.6.2",
plugin.getString("dependency"));
+ Assertions.assertEquals("[4.22.0,4.23.0)",
plugin.getString("camelVersions"));
+ Assertions.assertEquals("inferred",
plugin.getString("camelVersionsSource"));
+ }
+
+ @Test
+ public void shouldSelectDeclaredRange() throws Exception {
+ downloader.release("2.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("1.6.2", "4.22.2", null);
+
+ TestPluginAdd command = newFixtureCommand();
+ command.cli = "4.23.0-SNAPSHOT";
+ Assertions.assertEquals(0, command.doCall());
+
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:2.0.0",
plugin.getString("dependency"));
+ Assertions.assertEquals("[4.23.0,4.24.0)",
plugin.getString("camelVersions"));
+ Assertions.assertEquals("declared",
plugin.getString("camelVersionsSource"));
+ }
+
+ @Test
+ public void shouldNotChangeConfigWhenSelectedReleaseCannotBeDownloaded()
throws Exception {
+ downloader.release("1.6.2", "4.22.2", null);
+ String before = PluginHelper.getOrCreatePluginConfig().toJson();
+
+ TestPluginAdd command = newFixtureCommand();
+ command.downloadFails = true;
+ Assertions.assertEquals(1, command.doCall());
+
+ Assertions.assertTrue(
+ printer.getOutput().contains("Cannot download
org.example.plugins:camel-jbang-plugin-fixture:1.6.2"),
+ printer.getOutput());
+ Assertions.assertEquals(before,
PluginHelper.getOrCreatePluginConfig().toJson());
+ }
+
+ @Test
+ public void shouldUseLatestWhenNoReleaseMatches() throws Exception {
+ downloader.release("2.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("1.6.2", "4.22.2", null);
+
+ TestPluginAdd command = newFixtureCommand();
+ command.cli = "4.24.0";
+ Assertions.assertEquals(0, command.doCall());
+
+ String warning = printer.getOutput();
+ Assertions.assertTrue(warning.startsWith("WARN: No release of fixture
is built for Camel CLI 4.24.0 yet."
+ + " The newest, fixture
2.0.0, is built for Camel 4.23.0. Using LATEST."),
+ warning);
+ Assertions.assertTrue(warning.contains("Camel 4.23: fixture 2.0.0"),
warning);
+ Assertions.assertTrue(warning.contains("Camel 4.22: fixture 1.6.2"),
warning);
+ Assertions.assertTrue(warning.contains("camel plugin add fixture
--version <version>"), warning);
+ Assertions.assertTrue(command.downloaded.isEmpty());
+
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:LATEST",
plugin.getString("dependency"));
+ Assertions.assertNull(plugin.get("camelVersions"));
+ }
+
+ @Test
+ public void shouldSelectNearestOlderReleaseWhenBetweenCamelVersions()
throws Exception {
+ downloader.release("2.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("1.4.2", "4.18.5", null);
+
+ TestPluginAdd command = newFixtureCommand();
+ command.cli = "4.21.0";
+ Assertions.assertEquals(0, command.doCall());
+
+ Assertions.assertEquals("WARN: No release of fixture is built for
Camel CLI 4.21.0. Selected fixture 1.4.2,"
+ + " the newest release for an older Camel
version (built for Camel 4.18.5)."
+ + System.lineSeparator() + "Plugin fixture
added",
+ printer.getOutput());
+
Assertions.assertEquals(List.of("org.example.plugins:camel-jbang-plugin-fixture:1.4.2"),
command.downloaded);
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:1.4.2",
plugin.getString("dependency"));
+ Assertions.assertEquals("[4.18.0,4.19.0)",
plugin.getString("camelVersions"));
+ Assertions.assertEquals("inferred",
plugin.getString("camelVersionsSource"));
+ // the load-time check does not warn on this Camel version again
+ Assertions.assertEquals("4.21", plugin.getString("selectedForCamel"));
+ }
+
+ @Test
+ public void shouldUseLatestWhenCliIsOlderThanAllReleases() throws
Exception {
+ downloader.release("2.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("1.4.2", "4.18.5", null);
+
+ TestPluginAdd command = newFixtureCommand();
+ command.cli = "4.14.0";
+ Assertions.assertEquals(0, command.doCall());
+
+ String warning = printer.getOutput();
+ Assertions.assertTrue(warning.startsWith(
+ "WARN: No release of fixture is built for Camel CLI 4.14.0 or
an older Camel version. Using LATEST."),
+ warning);
+ Assertions.assertTrue(warning.contains("Camel 4.18: fixture 1.4.2"),
warning);
+ Assertions.assertTrue(command.downloaded.isEmpty());
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:LATEST",
plugin.getString("dependency"));
+ }
+
+ @Test
+ public void shouldUseLatestWhenNoReleaseTellsItsCamelVersions() throws
Exception {
+ downloader.release("2.0.0", null, null);
+
+ TestPluginAdd command = newFixtureCommand();
+ Assertions.assertEquals(0, command.doCall());
+
+ Assertions.assertEquals("WARN: No release of fixture tells which Camel
versions it supports. Using LATEST."
+ + System.lineSeparator() + "Plugin fixture
added",
+ printer.getOutput());
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:LATEST",
plugin.getString("dependency"));
+ }
+
+ @Test
+ public void shouldUseLatestWhenCompatibilityCannotBeChecked() throws
Exception {
+ downloader.listingDown = true;
+
+ TestPluginAdd command = newFixtureCommand();
+ Assertions.assertEquals(0, command.doCall());
+
+ Assertions.assertEquals("WARN: Could not check plugin compatibility
(Cannot resolve available versions of"
+ + "
org.example.plugins:camel-jbang-plugin-fixture). Using LATEST."
+ + System.lineSeparator() + "Plugin fixture
added",
+ printer.getOutput());
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:LATEST",
plugin.getString("dependency"));
+ }
+
+ @Test
+ public void shouldWarnWhenExplicitVersionIsBuiltForAnotherCamel() throws
Exception {
+ downloader.release("1.6.2", "4.22.2", null);
+
+ TestPluginAdd command = newFixtureCommand();
+ command.version = "1.6.2";
+ command.cli = "4.23.0";
+ Assertions.assertEquals(0, command.doCall());
+
+ Assertions.assertEquals("WARN: fixture 1.6.2 is built for Camel
[4.22.0,4.23.0); this CLI is 4.23.0"
+ + System.lineSeparator() + "Plugin fixture
added",
+ printer.getOutput());
+ Assertions.assertTrue(command.downloaded.isEmpty());
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:1.6.2",
plugin.getString("dependency"));
+ Assertions.assertNull(plugin.get("camelVersions"));
+ }
+
+ @Test
+ public void shouldWarnWhenGavIsBuiltForAnotherCamel() throws Exception {
+ downloader.release("1.6.2", "4.22.2", null);
+
+ TestPluginAdd command = newCommand();
+ command.name = "fixture";
+ command.gav = "org.example.plugins:camel-jbang-plugin-fixture:1.6.2";
+ command.cli = "4.23.0";
+ Assertions.assertEquals(0, command.doCall());
+
+ Assertions.assertEquals("WARN: fixture 1.6.2 is built for Camel
[4.22.0,4.23.0); this CLI is 4.23.0"
+ + System.lineSeparator() + "Plugin fixture
added",
+ printer.getOutput());
+ JsonObject plugin = fixtureEntry();
+
Assertions.assertEquals("org.example.plugins:camel-jbang-plugin-fixture:1.6.2",
plugin.getString("dependency"));
+ }
+
+ @Test
+ public void shouldNotWarnWhenExplicitVersionMatches() throws Exception {
+ downloader.release("1.6.2", "4.22.2", null);
+
+ TestPluginAdd command = newFixtureCommand();
+ command.version = "1.6.2";
+ Assertions.assertEquals(0, command.doCall());
+
+ Assertions.assertEquals("Plugin fixture added", printer.getOutput());
+ }
+
+ private static JsonObject fixtureEntry() {
+ JsonObject plugins =
PluginHelper.getOrCreatePluginConfig().getMap("plugins");
+ return plugins.getMap("fixture");
+ }
+
+ private TestPluginAdd newCommand() {
+ return new TestPluginAdd(new CamelJBangMain().withPrinter(printer),
downloader);
+ }
+
+ // a community plugin under its own groupId
+ private TestPluginAdd newFixtureCommand() {
+ TestPluginAdd command = newCommand();
+ command.name = "fixture";
+ command.groupId = "org.example.plugins";
+ command.artifactId = "camel-jbang-plugin-fixture";
+ return command;
+ }
+
+ /**
+ * Reads the releases from a fake downloader, and does not download the
plugin.
+ */
+ private static class TestPluginAdd extends PluginAdd {
+
+ private final DependencyDownloader downloader;
+ private final List<String> downloaded = new ArrayList<>();
+ private String cli = "4.22.0";
+ private boolean downloadFails;
+
+ TestPluginAdd(CamelJBangMain main, DependencyDownloader downloader) {
+ super(main);
+ this.downloader = downloader;
+ }
+
+ @Override
+ String cliVersion() {
+ return cli;
+ }
+
+ @Override
+ DependencyDownloader newDownloader() {
+ return downloader;
+ }
+
+ @Override
+ Optional<Plugin> downloadPlugin(String dependency) {
+ downloaded.add(dependency);
+ return downloadFails ? Optional.empty() : Optional.of(new
CachedFakePlugin());
+ }
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/FakePluginDownloader.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/FakePluginDownloader.java
new file mode 100644
index 000000000000..14a36d3213ab
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/FakePluginDownloader.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.main.download.DownloadException;
+import org.apache.camel.main.download.MavenDependencyDownloader;
+import org.apache.camel.tooling.maven.MavenArtifactDescriptor;
+
+/**
+ * A downloader with the releases of a fixture plugin, without Maven.
+ */
+public class FakePluginDownloader extends MavenDependencyDownloader {
+
+ public final List<String> versions = new ArrayList<>();
+ public final Map<String, MavenArtifactDescriptor> descriptors = new
HashMap<>();
+ public final List<String> descriptorsRead = new ArrayList<>();
+ public boolean listingDown;
+ public boolean descriptorsDown;
+ public Boolean listedFresh;
+
+ public FakePluginDownloader release(String version, String
camelJBangCoreVersion, String camelVersions) {
+ versions.add(version);
+ descriptors.put(version,
PluginCompatibilityTest.descriptor(camelJBangCoreVersion, camelVersions));
+ return this;
+ }
+
+ @Override
+ public List<String> resolveAvailableVersions(String groupId, String
artifactId, boolean fresh) {
+ listedFresh = fresh;
+ if (listingDown) {
+ throw new DownloadException("Cannot resolve available versions of
" + groupId + ":" + artifactId);
+ }
+ return new ArrayList<>(versions);
+ }
+
+ @Override
+ public MavenArtifactDescriptor resolveArtifactDescriptor(
+ String groupId, String artifactId, String version, String repos) {
+ descriptorsRead.add(version);
+ MavenArtifactDescriptor answer = descriptors.get(version);
+ if (descriptorsDown || answer == null) {
+ throw new DownloadException("cannot download the POM of " +
version);
+ }
+ return answer;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginCompatibilityTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginCompatibilityTest.java
new file mode 100644
index 000000000000..7dc2321738b6
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginCompatibilityTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Compat;
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Source;
+import org.apache.camel.tooling.maven.MavenArtifactDescriptor;
+import org.apache.camel.tooling.maven.MavenGav;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PluginCompatibilityTest {
+
+ @Test
+ void declaredRange() {
+ Compat compat = PluginCompatibility.of(descriptor("4.23.0",
"[4.23.0,4.24.0)"));
+ assertThat(compat.source()).isEqualTo(Source.DECLARED);
+ assertThat(compat.range()).isEqualTo("[4.23.0,4.24.0)");
+ assertThat(compat.builtFor()).isEqualTo("4.23.0");
+ }
+
+ @Test
+ void declaredRangeWinsOverDependency() {
+ // a plugin built with 4.22 that also supports 4.23
+ Compat compat = PluginCompatibility.of(descriptor("4.22.2",
"[4.22.0,4.24.0)"));
+ assertThat(compat.source()).isEqualTo(Source.DECLARED);
+ assertThat(PluginCompatibility.accepts(compat.range(),
"4.23.1")).isTrue();
+ }
+
+ @Test
+ void inferredFromCamelJBangCore() {
+ Compat compat = PluginCompatibility.of(descriptor("4.22.2", null));
+ assertThat(compat.source()).isEqualTo(Source.INFERRED);
+ assertThat(compat.range()).isEqualTo("[4.22.0,4.23.0)");
+ assertThat(compat.builtFor()).isEqualTo("4.22.2");
+ assertThat(compat.describe()).isEqualTo("built for Camel 4.22.2");
+ }
+
+ @Test
+ void inferredFromSnapshotDependency() {
+ Compat compat = PluginCompatibility.of(descriptor("4.23.0-SNAPSHOT",
null));
+ assertThat(compat.source()).isEqualTo(Source.INFERRED);
+ assertThat(compat.range()).isEqualTo("[4.23.0,4.24.0)");
+ }
+
+ @Test
+ void unknown() {
+ Compat compat = PluginCompatibility.of(descriptor(null, null));
+ assertThat(compat.source()).isEqualTo(Source.UNKNOWN);
+ assertThat(compat.range()).isNull();
+ assertThat(compat.builtFor()).isNull();
+ }
+
+ @Test
+ void malformedRangeIsUnknown() {
+ assertThat(PluginCompatibility.of(descriptor("4.22.2",
"[4.23.0,")).source()).isEqualTo(Source.UNKNOWN);
+ assertThat(PluginCompatibility.of(descriptor("4.22.2", "not a
range")).source()).isEqualTo(Source.UNKNOWN);
+ // a single version is not a range
+ assertThat(PluginCompatibility.of(descriptor("4.22.2",
"4.23.0")).source()).isEqualTo(Source.UNKNOWN);
+ }
+
+ @Test
+ void acceptsBoundaries() {
+ String range = "[4.22.0,4.23.0)";
+ assertThat(PluginCompatibility.accepts(range, "4.22.0")).isTrue();
+ assertThat(PluginCompatibility.accepts(range, "4.22.9")).isTrue();
+ assertThat(PluginCompatibility.accepts(range, "4.23.0")).isFalse();
+ assertThat(PluginCompatibility.accepts(range, "4.21.5")).isFalse();
+ }
+
+ @Test
+ void acceptsSnapshotCli() {
+ assertThat(PluginCompatibility.accepts("[4.23.0,4.24.0)",
"4.23.0-SNAPSHOT")).isTrue();
+ assertThat(PluginCompatibility.accepts("[4.23.0,4.24.0)",
"4.24.0-SNAPSHOT")).isFalse();
+ }
+
+ @Test
+ void acceptsInvalid() {
+ assertThat(PluginCompatibility.accepts(null, "4.23.0")).isFalse();
+ assertThat(PluginCompatibility.accepts("[4.23.0,",
"4.23.0")).isFalse();
+ assertThat(PluginCompatibility.accepts("[4.23.0,4.24.0)",
null)).isFalse();
+ }
+
+ @Test
+ void isOlder() {
+ String range = "[4.22.0,4.23.0)";
+ assertThat(PluginCompatibility.isOlder(range, "4.23.0")).isTrue();
+ assertThat(PluginCompatibility.isOlder(range,
"4.24.0-SNAPSHOT")).isTrue();
+ assertThat(PluginCompatibility.isOlder(range, "4.22.9")).isFalse();
+ assertThat(PluginCompatibility.isOlder(range, "4.21.0")).isFalse();
+ assertThat(PluginCompatibility.isOlder("[4.22.0,4.23.0]",
"4.23.0")).isFalse();
+ assertThat(PluginCompatibility.isOlder("[4.22.0,)",
"5.0.0")).isFalse();
+ assertThat(PluginCompatibility.isOlder("[4.22.0,", "5.0.0")).isFalse();
+ }
+
+ @Test
+ void compareUpperBounds() {
+ assertThat(PluginCompatibility.compareUpperBounds("[4.20.0,4.21.0)",
"[4.22.0,4.23.0)")).isNegative();
+ assertThat(PluginCompatibility.compareUpperBounds("[4.22.0,4.23.0)",
"[4.18.0,4.19.0)")).isPositive();
+ assertThat(PluginCompatibility.compareUpperBounds("[4.22.0,4.23.0)",
"[4.21.0,4.23.0)")).isZero();
+ assertThat(PluginCompatibility.compareUpperBounds("[4.22.0,)",
"[4.21.0,4.23.0)")).isPositive();
+ }
+
+ @Test
+ void minorLine() {
+ assertThat(PluginCompatibility.minorLine("4.22.2")).isEqualTo("4.22");
+
assertThat(PluginCompatibility.minorLine("4.23.0-SNAPSHOT")).isEqualTo("4.23");
+ assertThat(PluginCompatibility.minorLine("foo")).isNull();
+ assertThat(PluginCompatibility.minorLine(null)).isNull();
+ }
+
+ public static MavenArtifactDescriptor descriptor(String
camelJBangCoreVersion, String camelVersions) {
+ List<MavenGav> deps = new ArrayList<>();
+ deps.add(dependency("org.example", "other", "1.0.0", "compile"));
+ if (camelJBangCoreVersion != null) {
+ deps.add(dependency("org.apache.camel", "camel-jbang-core",
camelJBangCoreVersion, "provided"));
+ }
+ Map<String, String> props = new LinkedHashMap<>();
+ if (camelVersions != null) {
+ props.put(PluginCompatibility.CAMEL_VERSIONS_PROPERTY,
camelVersions);
+ }
+ return new MavenArtifactDescriptor(
+ MavenGav.fromCoordinates("org.example.plugins",
"camel-jbang-plugin-fixture", "1.0.0", null, null), deps,
+ props);
+ }
+
+ private static MavenGav dependency(String groupId, String artifactId,
String version, String scope) {
+ MavenGav gav = MavenGav.fromCoordinates(groupId, artifactId, version,
null, null);
+ gav.setScope(scope);
+ return gav;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
index cff501ef7210..969a0fa687c1 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
@@ -285,6 +285,78 @@ public class PluginHelperTest {
assertTrue(main.quitCalled, "older build than plugin firstVersion must
be rejected");
}
+ @Test
+ void testCompatibilityWarningWhenRangeExcludesCli() throws Exception {
+ // a plugin release selected for an older CLI; loading it uses the
cache only, so there is no download
+ Path jar = tempDir.resolve("fake-plugin.jar");
+ FakePluginJar.write(jar, "compat");
+ String camelVersion = new
org.apache.camel.catalog.DefaultCamelCatalog().getCatalogVersion();
+ JsonObject entry = buildEntry("compat", camelVersion, jar,
Files.getLastModifiedTime(jar).toMillis());
+ entry.put("camelVersions", "[4.0.0,4.1.0)");
+ entry.put("camelVersionsSource", "inferred");
+ writeConfig(entry);
+
+ StringPrinter printer = new StringPrinter();
+ CamelJBangMain main = new CamelJBangMain().withPrinter(printer);
+ Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main,
null, "compat");
+ assertEquals(1, plugins.size());
+ assertEquals("WARN: compat is built for Camel [4.0.0,4.1.0) but this
CLI is " + camelVersion
+ + ". Run 'camel plugin add compat' to select a matching
release.",
+ printer.getOutput());
+
+ // once
+ PluginHelper.getActivePlugins(main, null, "compat");
+ assertEquals(1, printer.getOutput().lines().count());
+ }
+
+ @Test
+ void testCompatibilityWarningNamesThePluginVersion() {
+ StringPrinter printer = new StringPrinter();
+ JsonObject entry = new JsonObject();
+ entry.put("dependency",
"org.example.plugins:camel-jbang-plugin-fixture:1.6.2");
+ entry.put("camelVersions", "[4.22.0,4.23.0)");
+ PluginHelper.compatibilityCheck(printer, "4.23.0", "fixture-named",
entry);
+ assertEquals("WARN: fixture-named 1.6.2 is built for Camel
[4.22.0,4.23.0) but this CLI is 4.23.0."
+ + " Run 'camel plugin add fixture-named' to select a
matching release.",
+ printer.getOutput());
+ }
+
+ @Test
+ void testNoCompatibilityWarningWhenRangeIncludesCli() {
+ StringPrinter printer = new StringPrinter();
+ JsonObject entry = new JsonObject();
+ entry.put("dependency",
"org.example.plugins:camel-jbang-plugin-fixture:1.6.2");
+ entry.put("camelVersions", "[4.22.0,4.23.0)");
+ PluginHelper.compatibilityCheck(printer, "4.22.3-SNAPSHOT",
"fixture-ok", entry);
+ assertEquals("", printer.getOutput());
+ }
+
+ @Test
+ void
testNoCompatibilityWarningForNearestOlderReleaseOnTheSameCamelVersion() {
+ StringPrinter printer = new StringPrinter();
+ JsonObject entry = new JsonObject();
+ entry.put("dependency",
"org.example.plugins:camel-jbang-plugin-fixture:1.4.2");
+ entry.put("camelVersions", "[4.18.0,4.19.0)");
+ entry.put("selectedForCamel", "4.21");
+ PluginHelper.compatibilityCheck(printer, "4.21.3", "fixture-older",
entry);
+ assertEquals("", printer.getOutput());
+ // the CLI moved to another Camel version, where a matching release
may exist
+ PluginHelper.compatibilityCheck(printer, "4.22.0", "fixture-older",
entry);
+ assertEquals("WARN: fixture-older 1.4.2 is built for Camel
[4.18.0,4.19.0) but this CLI is 4.22.0."
+ + " Run 'camel plugin add fixture-older' to select a
matching release.",
+ printer.getOutput());
+ }
+
+ @Test
+ void testNoCompatibilityWarningForLegacyEntry() {
+ // registered before the Camel versions were stored
+ StringPrinter printer = new StringPrinter();
+ JsonObject entry = new JsonObject();
+ entry.put("dependency",
"org.example.plugins:camel-jbang-plugin-fixture:LATEST");
+ PluginHelper.compatibilityCheck(printer, "4.23.0", "fixture-legacy",
entry);
+ assertEquals("", printer.getOutput());
+ }
+
private void writeConfig(JsonObject pluginEntry) throws Exception {
JsonObject plugins = new JsonObject();
plugins.put(pluginEntry.getString("name"), pluginEntry);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginVersionResolverTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginVersionResolverTest.java
new file mode 100644
index 000000000000..96071d1491fa
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginVersionResolverTest.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.common.PluginCompatibility.Source;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.NoMatch;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Older;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Reason;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Result;
+import org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Selected;
+import
org.apache.camel.dsl.jbang.core.common.PluginVersionResolver.Unavailable;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PluginVersionResolverTest {
+
+ private static final String GROUP = "org.example.plugins";
+ private static final String ARTIFACT = "camel-jbang-plugin-fixture";
+
+ @Test
+ void selectsNewestPatchOfTheLine() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("2.0.1", "4.22.2", null)
+ .release("2.0.0", "4.22.0", null)
+ .release("1.0.0", "4.18.5", null);
+
+ Result result = resolve(downloader, "4.22.0");
+
+ assertThat(result).isInstanceOfSatisfying(Selected.class, s -> {
+ assertThat(s.version()).isEqualTo("2.0.1");
+ assertThat(s.compat().source()).isEqualTo(Source.INFERRED);
+ assertThat(s.compat().range()).isEqualTo("[4.22.0,4.23.0)");
+ assertThat(s.compat().builtFor()).isEqualTo("4.22.2");
+ });
+ assertThat(downloader.descriptorsRead).containsExactly("2.0.1");
+ }
+
+ @Test
+ void listsTheReleasesFresh() {
+ FakePluginDownloader downloader = new
FakePluginDownloader().release("1.0.0", "4.22.2", null);
+
+ resolve(downloader, "4.22.0");
+
+ // a new release must be found when plugin add runs again; only the
listing is fresh
+ assertThat(downloader.listedFresh).isTrue();
+ }
+
+ @Test
+ void readsOnlyTheNewestReleaseOfEachLine() {
+ // the releases of Forage on Maven Central, and one more line for 4.23
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("1.7.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("1.6.2", "4.22.2", null)
+ .release("1.6.1", "4.22.1", null)
+ .release("1.6.0", "4.22.0", null)
+ .release("1.5.0", "4.20.0", null)
+ .release("1.4.2", "4.18.5", null)
+ .release("1.4.1", "4.18.4", null)
+ .release("1.4.0", "4.18.3", null)
+ .release("1.3", "4.16.0", null)
+ .release("1.2", "4.14.0", null)
+ .release("1.1", "4.12.0", null)
+ .release("1.0", "4.10.0", null);
+
+ Result result = resolve(downloader, "4.18.2");
+
+ assertThat(result).isInstanceOfSatisfying(Selected.class, s ->
assertThat(s.version()).isEqualTo("1.4.2"));
+ assertThat(downloader.descriptorsRead).containsExactly("1.7.0",
"1.6.2", "1.5.0", "1.4.2");
+ }
+
+ @Test
+ void findsADeepLineAmongManyReleases() {
+ // 30 releases: 3 patches of 10 lines, and the CLI's line is the
oldest one
+ FakePluginDownloader downloader = new FakePluginDownloader();
+ for (int line = 10; line >= 1; line--) {
+ for (int patch = 2; patch >= 0; patch--) {
+ downloader.release("1." + line + "." + patch, "4." + (12 +
line) + "." + patch, null);
+ }
+ }
+
+ Result result = resolve(downloader, "4.13.1");
+
+ assertThat(result).isInstanceOfSatisfying(Selected.class, s ->
assertThat(s.version()).isEqualTo("1.1.2"));
+
assertThat(downloader.descriptorsRead).hasSize(10).startsWith("1.10.2").endsWith("1.1.2");
+ }
+
+ @Test
+ void fallsBackToTheNextReleaseOfTheLineWhenAPomCannotBeRead() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("2.0.0", "4.22.0", null)
+ .release("1.0.0", "4.18.5", null);
+ // listed, but its POM cannot be read
+ downloader.versions.add(0, "2.0.1");
+
+ Result result = resolve(downloader, "4.22.3");
+
+ assertThat(result).isInstanceOfSatisfying(Selected.class, s ->
assertThat(s.version()).isEqualTo("2.0.0"));
+ assertThat(downloader.descriptorsRead).containsExactly("2.0.1",
"2.0.0");
+ }
+
+ @Test
+ void readsAtMostTenLines() {
+ FakePluginDownloader downloader = new FakePluginDownloader();
+ for (int line = 12; line >= 1; line--) {
+ downloader.release("1." + line + ".0", "4." + (12 + line) + ".0",
null);
+ }
+
+ // only the oldest line (1.1, for Camel 4.13) supports the CLI, and it
is not read
+ Result result = resolve(downloader, "4.13.0");
+
+
assertThat(downloader.descriptorsRead).hasSize(PluginVersionResolver.MAX_RELEASE_LINES);
+ assertThat(downloader.descriptorsRead.get(0)).isEqualTo("1.12.0");
+ assertThat(result).isInstanceOfSatisfying(NoMatch.class,
+ n -> assertThat(n.reason()).isEqualTo(Reason.CLI_OLDER));
+ }
+
+ @Test
+ void selectsDeclaredRange() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("3.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("2.0.2", "4.22.2", null);
+
+ Result result = resolve(downloader, "4.23.0-SNAPSHOT");
+
+ assertThat(result).isInstanceOfSatisfying(Selected.class, s -> {
+ assertThat(s.version()).isEqualTo("3.0.0");
+ assertThat(s.compat().source()).isEqualTo(Source.DECLARED);
+ assertThat(s.compat().range()).isEqualTo("[4.23.0,4.24.0)");
+ });
+ }
+
+ @Test
+ void selectsTheNearestOlderLineWhenBetweenLines() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("3.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("2.1.0", "4.20.1", null)
+ .release("2.0.0", "4.18.5", null);
+
+ // no release for 4.21 or 4.22
+ Result result = resolve(downloader, "4.21.0");
+
+ assertThat(result).isInstanceOfSatisfying(Older.class, o -> {
+ assertThat(o.version()).isEqualTo("2.1.0");
+ assertThat(o.compat().range()).isEqualTo("[4.20.0,4.21.0)");
+ assertThat(o.newestPerLine())
+ .containsExactly(Map.entry("4.23", "3.0.0"),
Map.entry("4.20", "2.1.0"), Map.entry("4.18", "2.0.0"));
+ });
+ }
+
+ @Test
+ void selectsTheNewestReleaseOfTheNearestOlderCamelVersion() {
+ // two release lines of the plugin for the same Camel version
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("3.0.0", "4.23.0", null)
+ .release("2.1.0", "4.18.5", null)
+ .release("2.0.0", "4.18.2", null);
+
+ Result result = resolve(downloader, "4.20.0");
+
+ assertThat(result).isInstanceOfSatisfying(Older.class, o ->
assertThat(o.version()).isEqualTo("2.1.0"));
+ }
+
+ @Test
+ void noMatchWhenCliIsNewerThanAllReleases() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("3.0.0", "4.23.0", "[4.23.0,4.24.0)")
+ .release("2.0.2", "4.22.2", null)
+ .release("2.0.1", "4.22.1", null);
+
+ Result result = resolve(downloader, "4.24.0");
+
+ assertThat(result).isInstanceOfSatisfying(NoMatch.class, n -> {
+ assertThat(n.reason()).isEqualTo(Reason.CLI_NEWER);
+ assertThat(n.newestVersion()).isEqualTo("3.0.0");
+ assertThat(n.newest().source()).isEqualTo(Source.DECLARED);
+ assertThat(n.newestPerLine()).containsExactly(Map.entry("4.23",
"3.0.0"), Map.entry("4.22", "2.0.2"));
+ });
+ }
+
+ @Test
+ void noMatchWhenCliIsOlderThanAllReleases() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("2.0.2", "4.22.2", null)
+ .release("1.0.0", "4.18.5", null);
+
+ Result result = resolve(downloader, "4.14.0");
+
+ assertThat(result).isInstanceOfSatisfying(NoMatch.class, n -> {
+ assertThat(n.reason()).isEqualTo(Reason.CLI_OLDER);
+ assertThat(n.newestVersion()).isEqualTo("2.0.2");
+ assertThat(n.newest().builtFor()).isEqualTo("4.22.2");
+ });
+ }
+
+ @Test
+ void noMatchWhenUnknown() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("2.0.0", null, null)
+ .release("1.0.0", null, null);
+
+ Result result = resolve(downloader, "4.23.0");
+
+ assertThat(result).isInstanceOfSatisfying(NoMatch.class, n -> {
+ assertThat(n.reason()).isEqualTo(Reason.UNKNOWN);
+ assertThat(n.newestVersion()).isEqualTo("2.0.0");
+ assertThat(n.newest().source()).isEqualTo(Source.UNKNOWN);
+ assertThat(n.newestPerLine()).isEmpty();
+ });
+ }
+
+ @Test
+ void releasesWithUnknownCompatibilityAreSkipped() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("3.0.0", null, null)
+ .release("2.0.0", "4.23.0", null)
+ .release("1.0.0", "4.18.0", null);
+
+ Result result = resolve(downloader, "4.20.0");
+
+ assertThat(result).isInstanceOfSatisfying(Older.class, o ->
assertThat(o.version()).isEqualTo("1.0.0"));
+ }
+
+ @Test
+ void skipsSnapshotsAndQualifiers() {
+ FakePluginDownloader downloader = new FakePluginDownloader()
+ .release("3.0.0-SNAPSHOT", "4.22.2", null)
+ .release("2.1.0-RC1", "4.22.2", null)
+ .release("2.0.0.Final", "4.22.2", null)
+ .release("1.0.0", "4.22.2", null);
+
+ Result result = resolve(downloader, "4.22.2");
+
+ assertThat(result).isInstanceOfSatisfying(Selected.class, s ->
assertThat(s.version()).isEqualTo("1.0.0"));
+ assertThat(downloader.descriptorsRead).containsExactly("1.0.0");
+ }
+
+ @Test
+ void sortsByMavenVersionOrder() {
+ assertThat(PluginVersionResolver.newestFirst(List.of("1.9.0",
"1.10.0", "1.2", "1.10.0", "2.0.0-SNAPSHOT")))
+ .containsExactly("1.10.0", "1.9.0", "1.2");
+ }
+
+ @Test
+ void releaseLine() {
+
assertThat(PluginVersionResolver.releaseLine("1.6.2")).isEqualTo("1.6");
+ assertThat(PluginVersionResolver.releaseLine("1.0")).isEqualTo("1.0");
+ assertThat(PluginVersionResolver.releaseLine("2")).isEqualTo("2");
+ }
+
+ @Test
+ void unavailableWhenTheReleasesCannotBeListed() {
+ FakePluginDownloader downloader = new
FakePluginDownloader().release("2.0.0", "4.22.0", null);
+ downloader.listingDown = true;
+
+ Result result = resolve(downloader, "4.22.1");
+
+ assertThat(result).isInstanceOfSatisfying(Unavailable.class,
+ u -> assertThat(u.reason()).contains("Cannot resolve available
versions"));
+ }
+
+ @Test
+ void unavailableWhenNoRelease() {
+ Result result = resolve(new FakePluginDownloader(), "4.22.1");
+
+ assertThat(result).isInstanceOf(Unavailable.class);
+ }
+
+ @Test
+ void unavailableWhenNoPomCanBeRead() {
+ FakePluginDownloader downloader = new
FakePluginDownloader().release("2.0.0", "4.22.0", null);
+ downloader.descriptorsDown = true;
+
+ Result result = resolve(downloader, "4.22.1");
+
+ assertThat(result).isInstanceOf(Unavailable.class);
+ }
+
+ private static Result resolve(FakePluginDownloader downloader, String
cliVersion) {
+ return new PluginVersionResolver(downloader).resolve(GROUP, ARTIFACT,
cliVersion);
+ }
+}
diff --git
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
index 81ef9f60ad57..430544a10f6d 100644
---
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
+++
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
@@ -22,6 +22,7 @@ import java.util.List;
import org.apache.camel.CamelContextAware;
import org.apache.camel.StaticService;
import org.apache.camel.tooling.maven.MavenArtifact;
+import org.apache.camel.tooling.maven.MavenArtifactDescriptor;
import org.apache.camel.tooling.maven.RepositoryResolver;
/**
@@ -200,6 +201,37 @@ public interface DependencyDownloader extends
CamelContextAware, StaticService {
*/
List<String[]> resolveAvailableVersions(String groupId, String artifactId,
String minimumVersion, String repo);
+ /**
+ * Resolves the available versions of the given maven artifact from all
the repositories this downloader is
+ * configured with: Maven Central (unless disabled), the repositories of
the active profiles in the Maven settings
+ * (with mirrors applied), and the configured repositories (see {@link
#setRepositories(String)}). A repository that
+ * cannot be reached is skipped.
+ *
+ * @param groupId maven group id
+ * @param artifactId maven artifact id
+ * @param fresh whether to check the remote repositories
again, instead of using the metadata
+ * downloaded before
+ * @return the versions, in no particular order
+ * @throws DownloadException when none of the repositories can be reached
+ */
+ default List<String> resolveAvailableVersions(String groupId, String
artifactId, boolean fresh) {
+ throw new UnsupportedOperationException("resolveAvailableVersions is
not supported by " + getClass().getName());
+ }
+
+ /**
+ * Reads the resolved (effective) POM of the given maven artifact, without
downloading the artifact itself.
+ *
+ * @param groupId maven group id
+ * @param artifactId maven artifact id
+ * @param version maven version
+ * @param repos optional comma separated list of additional maven
repositories to use
+ * @return the artifact descriptor with its dependencies and
properties
+ */
+ default MavenArtifactDescriptor resolveArtifactDescriptor(
+ String groupId, String artifactId, String version, String repos) {
+ throw new UnsupportedOperationException("resolveArtifactDescriptor is
not supported by " + getClass().getName());
+ }
+
/**
* Checks whether the dependency is already on the classpath
*
diff --git
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
index 3319eecd4dfd..5ec4b2b4b7a1 100644
---
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
+++
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
@@ -46,6 +46,7 @@ import org.apache.camel.main.util.XmlHelper;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.support.service.ServiceSupport;
import org.apache.camel.tooling.maven.MavenArtifact;
+import org.apache.camel.tooling.maven.MavenArtifactDescriptor;
import org.apache.camel.tooling.maven.MavenDownloader;
import org.apache.camel.tooling.maven.MavenDownloaderImpl;
import org.apache.camel.tooling.maven.MavenGav;
@@ -429,6 +430,40 @@ public class MavenDependencyDownloader extends
ServiceSupport implements Depende
return answer;
}
+ @Override
+ public List<String> resolveAvailableVersions(String groupId, String
artifactId, boolean fresh) {
+ if (verbose) {
+ LOG.info("Downloading available versions: {}:{}", groupId,
artifactId);
+ } else {
+ LOG.debug("Downloading available versions: {}:{}", groupId,
artifactId);
+ }
+ try {
+ List<String> answer = new ArrayList<>();
+ for (MavenGav gav :
mavenDownloader.resolveAvailableVersions(groupId, artifactId, null, fresh)) {
+ answer.add(gav.getVersion());
+ }
+ return answer;
+ } catch (MavenResolutionException e) {
+ throw new DownloadException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public MavenArtifactDescriptor resolveArtifactDescriptor(
+ String groupId, String artifactId, String version, String repos) {
+ String gav = groupId + ":" + artifactId + ":" + version;
+ if (verbose) {
+ LOG.info("Reading artifact descriptor: {}", gav);
+ } else {
+ LOG.debug("Reading artifact descriptor: {}", gav);
+ }
+ try {
+ return mavenDownloader.resolveArtifactDescriptor(gav,
resolveExtraRepositories(repos));
+ } catch (MavenResolutionException e) {
+ throw new DownloadException(e.getMessage(), e);
+ }
+ }
+
private void resolveQuarkus(String minimumVersion, String camelVersion,
Set<String> extraRepos, List<String[]> answer)
throws Exception {
if (VersionHelper.isGE(camelVersion, MINIMUM_QUARKUS_VERSION)) {
diff --git a/tooling/camel-tooling-maven/pom.xml
b/tooling/camel-tooling-maven/pom.xml
index dca23f7e5d57..b0907e5e9ebf 100644
--- a/tooling/camel-tooling-maven/pom.xml
+++ b/tooling/camel-tooling-maven/pom.xml
@@ -99,6 +99,12 @@
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-xml</artifactId>
</dependency>
+ <!-- to read the effective model properties of an artifact descriptor
(also a dependency of standalone-static) -->
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-resolver-provider</artifactId>
+ <version>${maven-version}</version>
+ </dependency>
<!-- testing -->
<dependency>
diff --git
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenArtifactDescriptor.java
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenArtifactDescriptor.java
new file mode 100644
index 000000000000..94ae10c397e1
--- /dev/null
+++
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenArtifactDescriptor.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.tooling.maven;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The resolved (effective) POM of a Maven artifact: its direct dependencies,
with versions and scopes after parent
+ * inheritance and dependency management (including imported BOMs) are
applied, and its effective model properties.
+ *
+ * @param gav the artifact
+ * @param dependencies the direct dependencies, each with its scope set (see
{@link MavenGav#getScope()})
+ * @param properties the effective model properties, with interpolation
applied. Values that could not be interpolated
+ * (still containing <code>${</code>) are left out.
+ */
+public record MavenArtifactDescriptor(MavenGav gav, List<MavenGav>
dependencies, Map<String, String> properties) {
+
+ /**
+ * Finds a direct dependency by groupId and artifactId (any scope).
+ */
+ public MavenGav findDependency(String groupId, String artifactId) {
+ for (MavenGav dep : dependencies) {
+ if (groupId.equals(dep.getGroupId()) &&
artifactId.equals(dep.getArtifactId())) {
+ return dep;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
index 0a06b732d336..eade50acf9a6 100644
---
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
+++
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
@@ -80,6 +80,39 @@ public interface MavenDownloader extends Service {
List<MavenGav> resolveAvailableVersions(String groupId, String artifactId,
String repository)
throws MavenResolutionException;
+ /**
+ * Resolves the available versions for groupId + artifactId from all the
configured remote repositories: Maven
+ * Central (unless disabled), the repositories of the active profiles in
the Maven settings, and the configured
+ * repositories (see {@link #setRepos(String)}), with mirrors applied,
plus the given extra repositories. The
+ * versions of all repositories are merged, and a repository that cannot
be reached is skipped.
+ *
+ * @param groupId groupId
+ * @param artifactId artifactId
+ * @param extraRepositories nullable set of additional
repositories to use
+ * @param fresh whether to check the remote
repositories again, instead of using the metadata
+ * downloaded before
+ * @return the versions, in no particular order,
without duplicates
+ * @throws MavenResolutionException when none of the repositories can be
reached
+ */
+ default List<MavenGav> resolveAvailableVersions(
+ String groupId, String artifactId, Set<String> extraRepositories,
boolean fresh)
+ throws MavenResolutionException {
+ throw new UnsupportedOperationException("resolveAvailableVersions is
not supported by " + getClass().getName());
+ }
+
+ /**
+ * Reads the resolved (effective) POM of a single artifact, without
downloading the artifact itself or its
+ * dependencies. Uses the same repositories, mirrors, settings and offline
mode as
+ * {@link #resolveArtifacts(List, Set, boolean, boolean)}.
+ *
+ * @param gav the Maven coordinates ({@code
groupId:artifactId:version})
+ * @param extraRepositories nullable set of additional repositories to use
(except the discovered ones)
+ */
+ default MavenArtifactDescriptor resolveArtifactDescriptor(String gav,
Set<String> extraRepositories)
+ throws MavenResolutionException {
+ throw new UnsupportedOperationException("resolveArtifactDescriptor is
not supported by " + getClass().getName());
+ }
+
/**
* Existing, configured {@link MavenDownloader} can be used as a template
to create customized version which shares
* most of the configuration except underlying {@code
org.eclipse.aether.RepositorySystemSession}, which can't be
diff --git
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
index 66f3ecd2351d..9cec02706eda 100644
---
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
+++
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
@@ -26,8 +26,10 @@ import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -42,6 +44,8 @@ import org.apache.camel.util.StopWatch;
import org.apache.maven.artifact.repository.metadata.Metadata;
import org.apache.maven.artifact.repository.metadata.Versioning;
import
org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
+import org.apache.maven.model.Model;
+import org.apache.maven.repository.internal.ArtifactDescriptorReaderDelegate;
import org.apache.maven.settings.Profile;
import org.apache.maven.settings.Repository;
import org.apache.maven.settings.Settings;
@@ -61,6 +65,9 @@ import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.LocalRepositoryManager;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
@@ -69,6 +76,9 @@ import
org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.resolution.MetadataRequest;
import org.eclipse.aether.resolution.MetadataResult;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -92,6 +102,8 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
true, RepositoryPolicy.UPDATE_POLICY_ALWAYS,
RepositoryPolicy.CHECKSUM_POLICY_WARN);
private static final RepositoryPolicy POLICY_DISABLED = new
RepositoryPolicy(
false, RepositoryPolicy.UPDATE_POLICY_NEVER,
RepositoryPolicy.CHECKSUM_POLICY_IGNORE);
+ // see org.eclipse.aether.internal.impl.DefaultUpdateCheckManager
+ private static final String UPDATE_CHECK_SESSION_STATE =
"aether.updateCheckManager.sessionState";
private static final DependencyFilter ACCEPT_ALL = (node, parents) -> true;
// Configuration
@@ -583,19 +595,7 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
StopWatch watch = new StopWatch();
- List<RemoteRepository> repositories = new
ArrayList<>(remoteRepositories);
- if (useApacheSnapshots && apacheSnapshotsResolutionRepository != null)
{
- repositories.add(apacheSnapshotsResolutionRepository);
- }
-
- // Add extra repositories if specified
- if (extraRepositories != null && !extraRepositories.isEmpty()) {
- List<RemoteRepository> extraRepos = new ArrayList<>();
- configureRepositories(extraRepos, extraRepositories);
- List<RemoteRepository> resolvedExtraRepos =
repositorySystem.newResolutionRepositories(
- session, extraRepos);
- repositories.addAll(resolvedExtraRepos);
- }
+ List<RemoteRepository> repositories =
resolutionRepositories(extraRepositories, useApacheSnapshots, session);
List<MavenArtifact> result = new ArrayList<>();
@@ -678,6 +678,95 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
}
}
+ /**
+ * The configured repositories (after mirroring), plus Apache Snapshots
and the extra repositories if requested.
+ */
+ private List<RemoteRepository> resolutionRepositories(
+ Set<String> extraRepositories, boolean useApacheSnapshots,
RepositorySystemSession session) {
+ List<RemoteRepository> repositories = new
ArrayList<>(remoteRepositories);
+ if (useApacheSnapshots && apacheSnapshotsResolutionRepository != null)
{
+ repositories.add(apacheSnapshotsResolutionRepository);
+ }
+
+ // Add extra repositories if specified
+ if (extraRepositories != null && !extraRepositories.isEmpty()) {
+ List<RemoteRepository> extraRepos = new ArrayList<>();
+ configureRepositories(extraRepos, extraRepositories);
+ List<RemoteRepository> resolvedExtraRepos =
repositorySystem.newResolutionRepositories(
+ session, extraRepos);
+ repositories.addAll(resolvedExtraRepos);
+ }
+ return repositories;
+ }
+
+ @Override
+ public MavenArtifactDescriptor resolveArtifactDescriptor(String gav,
Set<String> extraRepositories)
+ throws MavenResolutionException {
+ RepositorySystemSession session = preferLocal && !fresh ?
getPreferLocalSession() : repositorySystemSession;
+ List<RemoteRepository> repositories =
resolutionRepositories(extraRepositories, false, session);
+
+ DefaultRepositorySystemSession descriptorSession = new
DefaultRepositorySystemSession(session);
+ // fail on a missing or invalid POM, instead of returning an empty
descriptor
+ descriptorSession.setArtifactDescriptorPolicy(new
SimpleArtifactDescriptorPolicy(false, false));
+ if (!embeddedMode) {
+ // the default descriptor reader does not expose the model
properties, so a delegate copies them.
+ // Not in embedded mode, where Maven loads the reader (and its
delegate type) from its own class loader
+
descriptorSession.setConfigProperty(ArtifactDescriptorReaderDelegate.class.getName(),
+ new ModelPropertiesDelegate());
+ }
+
+ try {
+ ArtifactDescriptorRequest request = new
ArtifactDescriptorRequest(new DefaultArtifact(gav), repositories, null);
+ ArtifactDescriptorResult result =
repositorySystem.readArtifactDescriptor(descriptorSession, request);
+
+ List<MavenGav> dependencies = new ArrayList<>();
+ for (Dependency d : result.getDependencies()) {
+ Artifact a = d.getArtifact();
+ MavenGav dep = MavenGav.fromCoordinates(a.getGroupId(),
a.getArtifactId(), a.getVersion(),
+ a.getExtension(), a.getClassifier());
+ dep.setScope(d.getScope());
+ dependencies.add(dep);
+ }
+
+ Map<String, String> properties = new LinkedHashMap<>();
+ for (Map.Entry<String, Object> e :
result.getProperties().entrySet()) {
+ if (e.getKey().startsWith(ModelPropertiesDelegate.PREFIX) &&
e.getValue() != null) {
+ String value = e.getValue().toString();
+ // a value that could not be interpolated is of no use
+ if (!value.contains("${")) {
+
properties.put(e.getKey().substring(ModelPropertiesDelegate.PREFIX.length()),
value);
+ }
+ }
+ }
+
+ Artifact artifact = result.getArtifact();
+ MavenGav descriptorGav =
MavenGav.fromCoordinates(artifact.getGroupId(), artifact.getArtifactId(),
+ artifact.getVersion(), artifact.getExtension(),
artifact.getClassifier());
+ return new MavenArtifactDescriptor(descriptorGav, dependencies,
properties);
+ } catch (ArtifactDescriptorException e) {
+ MavenResolutionException mre = new
MavenResolutionException(e.getMessage(), e);
+ repositories.forEach(r -> mre.getRepositories().add(r.getUrl()));
+ throw mre;
+ }
+ }
+
+ /**
+ * Adds the effective model properties to the descriptor result
properties, under a prefix so they do not clash with
+ * the properties set by the default delegate (such as {@code
license.count}).
+ */
+ private static final class ModelPropertiesDelegate extends
ArtifactDescriptorReaderDelegate {
+
+ static final String PREFIX = "camel.model.property.";
+
+ @Override
+ public void populateResult(RepositorySystemSession session,
ArtifactDescriptorResult result, Model model) {
+ super.populateResult(session, result, model);
+ Map<String, Object> properties = new
LinkedHashMap<>(result.getProperties());
+ model.getProperties().forEach((k, v) -> properties.put(PREFIX + k,
v));
+ result.setProperties(properties);
+ }
+ }
+
@Override
public List<MavenGav> resolveAvailableVersions(String groupId, String
artifactId, String repository)
throws MavenResolutionException {
@@ -713,24 +802,7 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
List<MetadataResult> result =
repositorySystem.resolveMetadata(repositorySystemSession, List.of(req));
for (MetadataResult mr : result) {
if (mr.isResolved() && mr.getMetadata().getFile() != null) {
- File f = mr.getMetadata().getFile();
- if (f.exists() && f.isFile()) {
- MetadataXpp3Reader reader = new MetadataXpp3Reader();
- try (InputStream is = new
BufferedInputStream(Files.newInputStream(f.toPath()))) {
- Metadata md = reader.read(is);
- Versioning versioning = md.getVersioning();
- if (versioning != null) {
- List<String> versions =
versioning.getVersions();
- if (versions != null) {
- for (String v : versions) {
- if (v != null) {
-
gavs.add(MavenGav.fromCoordinates(groupId, artifactId, v, null, null));
- }
- }
- }
- }
- }
- }
+ readVersions(mr.getMetadata().getFile(), groupId,
artifactId, gavs);
}
}
return gavs;
@@ -742,6 +814,95 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
}
}
+ @Override
+ public List<MavenGav> resolveAvailableVersions(
+ String groupId, String artifactId, Set<String> extraRepositories,
boolean fresh)
+ throws MavenResolutionException {
+ List<RemoteRepository> repositories
+ = resolutionRepositories(extraRepositories, false,
repositorySystemSession);
+ RepositorySystemSession session = repositorySystemSession;
+ if (fresh) {
+ // check the remote repositories again, instead of using the
metadata downloaded before
+ DefaultRepositorySystemSession freshSession = new
DefaultRepositorySystemSession(repositorySystemSession);
+
freshSession.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS);
+ // also when checked before in this session
+ freshSession.setConfigProperty(UPDATE_CHECK_SESSION_STATE,
"bypass");
+ session = freshSession;
+ }
+
+ List<MetadataRequest> requests = new ArrayList<>();
+ for (RemoteRepository repository : repositories) {
+ MetadataRequest req = new MetadataRequest();
+ req.setRepository(repository);
+ req.setFavorLocalRepository(false);
+ req.setMetadata(new DefaultMetadata(
+ groupId, artifactId, "maven-metadata.xml",
+ org.eclipse.aether.metadata.Metadata.Nature.RELEASE));
+ requests.add(req);
+ }
+
+ // merge the versions of all repositories, and skip a repository that
cannot be reached
+ Set<String> versions = new LinkedHashSet<>();
+ List<Exception> failures = new ArrayList<>();
+ for (MetadataResult mr : repositorySystem.resolveMetadata(session,
requests)) {
+ Exception failure = mr.getException();
+
+ // not found means the repository does not have the artifact,
unless it cannot be accessed offline
+ boolean notFound = failure instanceof MetadataNotFoundException
+ && !(failure.getCause() instanceof
RepositoryOfflineException);
+ if (failure != null && !notFound) {
+ LOG.debug("Cannot resolve available versions of {}:{} in {}:
{}", groupId, artifactId,
+ mr.getRequest().getRepository().getUrl(),
failure.getMessage());
+ failures.add(failure);
+ continue;
+ }
+ if (mr.isResolved() && mr.getMetadata().getFile() != null) {
+ List<MavenGav> gavs = new ArrayList<>();
+ try {
+ readVersions(mr.getMetadata().getFile(), groupId,
artifactId, gavs);
+ } catch (Exception e) {
+ failures.add(e);
+ continue;
+ }
+ gavs.forEach(g -> versions.add(g.getVersion()));
+ }
+ }
+ if (!requests.isEmpty() && failures.size() == requests.size()) {
+ MavenResolutionException mre = new MavenResolutionException(
+ "Cannot resolve available versions of " + groupId + ":" +
artifactId + ": "
+ +
failures.get(0).getMessage(),
+ failures.get(0));
+ repositories.forEach(r -> mre.getRepositories().add(r.getUrl()));
+ throw mre;
+ }
+
+ List<MavenGav> answer = new ArrayList<>(versions.size());
+ for (String v : versions) {
+ answer.add(MavenGav.fromCoordinates(groupId, artifactId, v, null,
null));
+ }
+ return answer;
+ }
+
+ private static void readVersions(File f, String groupId, String
artifactId, List<MavenGav> gavs) throws Exception {
+ if (f.exists() && f.isFile()) {
+ MetadataXpp3Reader reader = new MetadataXpp3Reader();
+ try (InputStream is = new
BufferedInputStream(Files.newInputStream(f.toPath()))) {
+ Metadata md = reader.read(is);
+ Versioning versioning = md.getVersioning();
+ if (versioning != null) {
+ List<String> versions = versioning.getVersions();
+ if (versions != null) {
+ for (String v : versions) {
+ if (v != null) {
+ gavs.add(MavenGav.fromCoordinates(groupId,
artifactId, v, null, null));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
// ========== Configuration Methods ==========
@Override
diff --git
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
index d4f1106df3b1..1cdbe1467de3 100644
---
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
+++
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
@@ -701,4 +701,276 @@ class MavenDownloaderImplTest {
}
}
}
+
+ @Test
+ void testResolveArtifactDescriptor() throws Exception {
+ File repo = createDescriptorFixtureRepository();
+ File localRepo = new File(tempDir, "descriptor-local-repo");
+
+ try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+ downloader.setMavenCentralEnabled(false);
+ downloader.setMavenApacheSnapshotEnabled(false);
+ downloader.setMavenSettingsLocation("false");
+ downloader.setRepos(repo.toURI().toString());
+ downloader.build();
+ MavenDownloader customDownloader =
downloader.customize(localRepo.getAbsolutePath(), 5000, 5000);
+
+ MavenArtifactDescriptor descriptor
+ =
customDownloader.resolveArtifactDescriptor("org.example.fixture:fixture-plugin:1.0.0",
null);
+
+ assertEquals("org.example.fixture:fixture-plugin:1.0.0",
descriptor.gav().toString());
+
+ // the version comes from the BOM imported by the parent
+ MavenGav core = descriptor.findDependency("org.apache.camel",
"camel-jbang-core");
+ assertNotNull(core);
+ assertEquals("4.22.2", core.getVersion());
+ assertEquals("provided", core.getScope());
+ MavenGav other = descriptor.findDependency("org.example.fixture",
"fixture-lib");
+ assertNotNull(other);
+ assertEquals("compile", other.getScope());
+
+ // effective model properties: literal, interpolated, and
inherited from the parent
+ assertEquals("[4.22.0,4.23.0)",
descriptor.properties().get("camel.jbang.plugin.camel-versions"));
+ assertEquals("4.22.2",
descriptor.properties().get("fixture.derived"));
+ assertEquals("4.22.2",
descriptor.properties().get("camel.version"));
+ // a value that cannot be interpolated is left out
+
assertFalse(descriptor.properties().containsKey("fixture.unresolved"));
+ }
+ }
+
+ @Test
+ void testResolveArtifactDescriptorWithExtraRepository() throws Exception {
+ File repo = createDescriptorFixtureRepository();
+ File localRepo = new File(tempDir, "descriptor-extra-local-repo");
+
+ try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+ downloader.setMavenCentralEnabled(false);
+ downloader.setMavenApacheSnapshotEnabled(false);
+ downloader.setMavenSettingsLocation("false");
+ downloader.build();
+ MavenDownloader customDownloader =
downloader.customize(localRepo.getAbsolutePath(), 5000, 5000);
+
+ // not found without the extra repository
+ try {
+
customDownloader.resolveArtifactDescriptor("org.example.fixture:fixture-plugin:1.0.0",
null);
+ fail("Should not resolve the descriptor without the fixture
repository");
+ } catch (MavenResolutionException e) {
+ // expected
+ }
+
+ MavenArtifactDescriptor descriptor =
customDownloader.resolveArtifactDescriptor(
+ "org.example.fixture:fixture-plugin:1.0.0",
Set.of(repo.toURI().toString()));
+ assertEquals("4.22.2",
descriptor.findDependency("org.apache.camel", "camel-jbang-core").getVersion());
+ }
+ }
+
+ @Test
+ void testResolveAvailableVersionsFresh() throws Exception {
+ File repo = new File(tempDir, "versions-repo");
+ writeMetadata(repo, "1.0.0");
+ File localRepo = new File(tempDir, "versions-local-repo");
+
+ try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+ downloader.setMavenCentralEnabled(false);
+ downloader.setMavenApacheSnapshotEnabled(false);
+ downloader.setMavenSettingsLocation("false");
+ downloader.setRepos(repo.toURI().toString());
+ downloader.build();
+ MavenDownloader customDownloader =
downloader.customize(localRepo.getAbsolutePath(), 5000, 5000);
+
+ assertEquals(List.of("1.0.0"), versions(customDownloader, false));
+
+ // a new release is published
+ writeMetadata(repo, "1.0.0", "1.1.0");
+
+ // the metadata downloaded before is used
+ assertEquals(List.of("1.0.0"), versions(customDownloader, false));
+ // unless fresh
+ assertEquals(List.of("1.0.0", "1.1.0"), versions(customDownloader,
true));
+ }
+ }
+
+ @Test
+ void testResolveAvailableVersionsFromSettingsProfileRepository() throws
Exception {
+ File repo = new File(tempDir, "profile-repo");
+ writeMetadata(repo, "2.0.0", "2.1.0");
+ File settings = new File(tempDir, "profile-settings.xml");
+ Files.writeString(settings.toPath(), """
+ <settings>
+ <profiles>
+ <profile>
+ <id>plugins</id>
+ <repositories>
+ <repository>
+ <id>plugins-repo</id>
+ <url>%s</url>
+ </repository>
+ </repositories>
+ </profile>
+ </profiles>
+ <activeProfiles>
+ <activeProfile>plugins</activeProfile>
+ </activeProfiles>
+ </settings>
+ """.formatted(repo.toURI()));
+ File localRepo = new File(tempDir, "profile-local-repo");
+
+ try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+ downloader.setMavenCentralEnabled(false);
+ downloader.setMavenApacheSnapshotEnabled(false);
+ downloader.setMavenSettingsLocation(settings.getAbsolutePath());
+ downloader.build();
+ MavenDownloader customDownloader =
downloader.customize(localRepo.getAbsolutePath(), 5000, 5000);
+
+ assertEquals(List.of("2.0.0", "2.1.0"), versions(customDownloader,
true));
+ }
+ }
+
+ @Test
+ void testResolveAvailableVersionsSkipsUnreachableRepository() throws
Exception {
+ File repo = new File(tempDir, "reachable-repo");
+ writeMetadata(repo, "1.0.0");
+ File localRepo = new File(tempDir, "unreachable-local-repo");
+
+ try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+ downloader.setMavenCentralEnabled(false);
+ downloader.setMavenApacheSnapshotEnabled(false);
+ downloader.setMavenSettingsLocation("false");
+ downloader.setRepos(repo.toURI() + ",http://localhost:1/maven2");
+ downloader.build();
+ MavenDownloader customDownloader =
downloader.customize(localRepo.getAbsolutePath(), 2000, 2000);
+
+ assertEquals(List.of("1.0.0"), versions(customDownloader, true));
+ }
+ }
+
+ @Test
+ void testResolveAvailableVersionsFailsWhenNoRepositoryCanBeReached()
throws Exception {
+ File repo = new File(tempDir, "offline-repo");
+ writeMetadata(repo, "1.0.0");
+ File localRepo = new File(tempDir, "offline-local-repo");
+
+ try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+ downloader.setMavenCentralEnabled(false);
+ downloader.setMavenApacheSnapshotEnabled(false);
+ downloader.setMavenSettingsLocation("false");
+ downloader.setRepos(repo.toURI().toString());
+ // offline: no repository can be reached
+ downloader.setOffline(true);
+ downloader.build();
+ MavenDownloader customDownloader =
downloader.customize(localRepo.getAbsolutePath(), 2000, 2000);
+
+ try {
+ versions(customDownloader, true);
+ fail("Should fail when no repository can be reached");
+ } catch (MavenResolutionException e) {
+
assertTrue(e.getRepositories().contains(repo.toURI().toString()),
e.getRepositories().toString());
+ }
+ }
+ }
+
+ private static List<String> versions(MavenDownloader downloader, boolean
fresh) throws Exception {
+ return downloader.resolveAvailableVersions("org.example.fixture",
"fixture-plugin", null, fresh).stream()
+ .map(MavenGav::getVersion).sorted().toList();
+ }
+
+ private static void writeMetadata(File repo, String... versions) throws
Exception {
+ File dir = new File(repo, "org/example/fixture/fixture-plugin");
+ assertTrue(dir.mkdirs() || dir.isDirectory());
+ StringBuilder sb = new StringBuilder();
+ for (String v : versions) {
+ sb.append(" <version>").append(v).append("</version>\n");
+ }
+ Files.writeString(new File(dir, "maven-metadata.xml").toPath(), """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <metadata>
+ <groupId>org.example.fixture</groupId>
+ <artifactId>fixture-plugin</artifactId>
+ <versioning>
+ <versions>
+ %s </versions>
+ </versioning>
+ </metadata>
+ """.formatted(sb));
+ }
+
+ /**
+ * A file:// repository with a plugin whose parent imports a BOM that
manages camel-jbang-core.
+ */
+ private File createDescriptorFixtureRepository() throws Exception {
+ File repo = new File(tempDir, "descriptor-repo");
+ writePom(repo, "fixture-bom", """
+ <packaging>pom</packaging>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-jbang-core</artifactId>
+ <version>4.22.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.example.fixture</groupId>
+ <artifactId>fixture-lib</artifactId>
+ <version>1.0.0</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+ """);
+ writePom(repo, "fixture-parent", """
+ <packaging>pom</packaging>
+ <properties>
+ <camel.version>4.22.2</camel.version>
+ </properties>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.example.fixture</groupId>
+ <artifactId>fixture-bom</artifactId>
+ <version>1.0.0</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+ """);
+ writePom(repo, "fixture-plugin", """
+ <parent>
+ <groupId>org.example.fixture</groupId>
+ <artifactId>fixture-parent</artifactId>
+ <version>1.0.0</version>
+ </parent>
+ <properties>
+
<camel.jbang.plugin.camel-versions>[4.22.0,4.23.0)</camel.jbang.plugin.camel-versions>
+ <fixture.derived>${camel.version}</fixture.derived>
+ <fixture.unresolved>${does.not.exist}</fixture.unresolved>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-jbang-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.example.fixture</groupId>
+ <artifactId>fixture-lib</artifactId>
+ </dependency>
+ </dependencies>
+ """);
+ return repo;
+ }
+
+ private static void writePom(File repo, String artifactId, String body)
throws Exception {
+ File dir = new File(repo, "org/example/fixture/" + artifactId +
"/1.0.0");
+ assertTrue(dir.mkdirs() || dir.isDirectory());
+ String pom = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.example.fixture</groupId>
+ <artifactId>%s</artifactId>
+ <version>1.0.0</version>
+ %s</project>
+ """.formatted(artifactId, body);
+ Files.writeString(new File(dir, artifactId + "-1.0.0.pom").toPath(),
pom);
+ }
}