This is an automated email from the ASF dual-hosted git repository.
ldemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new b22abbe0fb8e CAMEL-23186: [camel-jbang-mcp]
CatalogLoader.loadCatalog() fails with NoClassDefFoundError for versioned
catalogs
b22abbe0fb8e is described below
commit b22abbe0fb8e4adeba59efae934b114ed7612705
Author: Luigi De Masi <[email protected]>
AuthorDate: Fri Mar 13 13:52:59 2026 +0100
CAMEL-23186: [camel-jbang-mcp] CatalogLoader.loadCatalog() fails with
NoClassDefFoundError for versioned catalogs
---
.../camel/dsl/jbang/core/common/CatalogLoader.java | 59 ++++++--
.../dsl/jbang/core/commands/mcp/CatalogTools.java | 41 +++++-
.../jbang/core/commands/mcp/CatalogToolsTest.java | 162 ++++++++++++++++++++-
3 files changed, 232 insertions(+), 30 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
index d7c91fb88563..9c1606de885c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
@@ -51,12 +51,21 @@ public final class CatalogLoader {
}
public static CamelCatalog loadCatalog(String repos, String version,
boolean download) throws Exception {
+ return loadCatalog(repos, version, null, download);
+ }
+
+ public static CamelCatalog loadCatalog(String repos, String version,
String catalogGroupId, boolean download)
+ throws Exception {
CamelCatalog answer = new DefaultCamelCatalog();
if (version == null || version.isEmpty() ||
version.equals(answer.getCatalogVersion())) {
answer.enableCache();
return answer;
}
+ if (catalogGroupId == null) {
+ catalogGroupId = "org.apache.camel";
+ }
+
DependencyDownloaderClassLoader cl = new
DependencyDownloaderClassLoader(null);
MavenDependencyDownloader downloader = new MavenDependencyDownloader();
downloader.setClassLoader(cl);
@@ -65,23 +74,24 @@ public final class CatalogLoader {
try {
downloader.start();
- // download camel-catalog for that specific version
- MavenArtifact ma = downloader.downloadArtifact("org.apache.camel",
"camel-catalog", version);
+ // download camel-core-catalog (contains
AbstractCachingCamelCatalog) and camel-catalog
+ MavenArtifact ma = downloader.downloadArtifact("org.apache.camel",
"camel-core-catalog", version);
if (ma != null) {
cl.addFile(ma.getFile());
} else {
- throw new IOException("Cannot download
org.apache.camel:camel-catalog:" + version);
+ throw new IOException("Cannot download
org.apache.camel:camel-core-catalog:" + version);
}
-
- // re-create answer with the classloader to be able to load
resources in this catalog
- Class<RuntimeProvider> clazz = (Class<RuntimeProvider>)
cl.loadClass(DEFAULT_CAMEL_CATALOG);
- if (clazz != null) {
- answer.setVersionManager(new
DownloadCatalogVersionManager(version, cl));
- RuntimeProvider provider = ObjectHelper.newInstance(clazz);
- if (provider != null) {
- answer.setRuntimeProvider(provider);
- }
+ ma = downloader.downloadArtifact(catalogGroupId, "camel-catalog",
version);
+ if (ma != null) {
+ cl.addFile(ma.getFile());
+ } else {
+ throw new IOException("Cannot download " + catalogGroupId +
":camel-catalog:" + version);
}
+
+ // use the downloaded classloader for catalog resource resolution
+ // keep the current DefaultCamelCatalog instance (which has the
correct getCatalogVersion behavior)
+ // rather than loading a new instance from the downloaded jar
+ answer.setVersionManager(new
DownloadCatalogVersionManager(version, cl));
answer.enableCache();
} finally {
downloader.stop();
@@ -92,11 +102,21 @@ public final class CatalogLoader {
}
public static CamelCatalog loadSpringBootCatalog(String repos, String
version, boolean download) throws Exception {
+ return loadSpringBootCatalog(repos, version, null, download);
+ }
+
+ public static CamelCatalog loadSpringBootCatalog(
+ String repos, String version, String springBootGroupId, boolean
download)
+ throws Exception {
CamelCatalog answer = new DefaultCamelCatalog();
if (version == null) {
version = answer.getCatalogVersion();
}
+ if (springBootGroupId == null) {
+ springBootGroupId = "org.apache.camel.springboot";
+ }
+
DependencyDownloaderClassLoader cl = new
DependencyDownloaderClassLoader(CatalogLoader.class.getClassLoader());
MavenDependencyDownloader downloader = new MavenDependencyDownloader();
downloader.setClassLoader(cl);
@@ -121,13 +141,13 @@ public final class CatalogLoader {
}
final String camelVersion = version;
- ma = downloader.downloadArtifact("org.apache.camel.springboot",
"camel-catalog-provider-springboot",
+ ma = downloader.downloadArtifact(springBootGroupId,
"camel-catalog-provider-springboot",
PropertyResolver.fromSystemProperty(CamelJBangConstants.CAMEL_SPRING_BOOT_VERSION,
() -> camelVersion));
if (ma != null) {
cl.addFile(ma.getFile());
} else {
throw new IOException(
- "Cannot download
org.apache.camel.springboot:camel-catalog-provider-springboot:" + version);
+ "Cannot download " + springBootGroupId +
":camel-catalog-provider-springboot:" + version);
}
Class<RuntimeProvider> clazz = (Class<RuntimeProvider>)
cl.loadClass(SPRING_BOOT_CATALOG_PROVIDER);
@@ -154,6 +174,12 @@ public final class CatalogLoader {
public static CamelCatalog loadQuarkusCatalog(String repos, String
quarkusVersion, String quarkusGroupId, boolean download)
throws Exception {
+ return loadQuarkusCatalog(repos, quarkusVersion, quarkusGroupId, null,
download);
+ }
+
+ public static CamelCatalog loadQuarkusCatalog(
+ String repos, String quarkusVersion, String quarkusGroupId, String
quarkusBomArtifactId, boolean download)
+ throws Exception {
String camelQuarkusVersion = null;
CamelCatalog answer = new DefaultCamelCatalog();
@@ -179,7 +205,10 @@ public final class CatalogLoader {
if (quarkusGroupId == null) {
quarkusGroupId = QUARKUS_GROUP_ID;
}
- MavenArtifact ma = downloader.downloadArtifact(quarkusGroupId,
"quarkus-camel-bom:pom", quarkusVersion);
+ if (quarkusBomArtifactId == null) {
+ quarkusBomArtifactId = "quarkus-camel-bom";
+ }
+ MavenArtifact ma = downloader.downloadArtifact(quarkusGroupId,
quarkusBomArtifactId + ":pom", quarkusVersion);
if (ma != null && ma.getFile() != null) {
String name = ma.getFile().getAbsolutePath();
File file = new File(name);
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
index 02baeb9bb801..c165c179c9d8 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
@@ -65,12 +65,16 @@ public class CatalogTools {
@ToolArg(description = "Version to query. For Main or Spring Boot:
the Camel version (e.g., 4.17.0). "
+ "For quarkus: the Quarkus Platform
version (e.g., 3.31.3) as returned by "
+ "camel_version_list quarkusVersion field.
"
- + "If not specified, uses the default
catalog version.") String camelVersion) {
+ + "If not specified, uses the default
catalog version.") String camelVersion,
+ @ToolArg(description = "Platform BOM coordinates in GAV format
(groupId:artifactId:version). "
+ + "When provided, overrides camelVersion.
For quarkus runtime, all three coordinates "
+ + "are used for BOM resolution. For main
and spring-boot, the version is extracted "
+ + "and used as the catalog version.")
String platformBom) {
int maxResults = limit != null ? limit : 50;
try {
- CamelCatalog cat = loadCatalog(runtime, camelVersion);
+ CamelCatalog cat = loadCatalog(runtime, camelVersion, platformBom);
List<ComponentInfo> components = findComponentNames(cat).stream()
.map(cat::componentModel)
@@ -106,14 +110,18 @@ public class CatalogTools {
@ToolArg(description = "Version to query. For Main or Spring Boot:
the Camel version (e.g., 4.17.0). "
+ "For quarkus: the Quarkus Platform
version (e.g., 3.31.3) as returned by "
+ "camel_version_list quarkusVersion field.
"
- + "If not specified, uses the default
catalog version.") String camelVersion) {
+ + "If not specified, uses the default
catalog version.") String camelVersion,
+ @ToolArg(description = "Platform BOM coordinates in GAV format
(groupId:artifactId:version). "
+ + "When provided, overrides camelVersion.
For quarkus runtime, all three coordinates "
+ + "are used for BOM resolution. For main
and spring-boot, the version is extracted "
+ + "and used as the catalog version.")
String platformBom) {
if (component == null || component.isBlank()) {
throw new ToolCallException("Component name is required", null);
}
try {
- CamelCatalog cat = loadCatalog(runtime, camelVersion);
+ CamelCatalog cat = loadCatalog(runtime, camelVersion, platformBom);
ComponentModel model = cat.componentModel(component);
if (model == null) {
// Check if it might be a data format or language instead
@@ -311,12 +319,32 @@ public class CatalogTools {
// Catalog loading
- private CamelCatalog loadCatalog(String runtime, String camelVersion)
throws Exception {
+ private CamelCatalog loadCatalog(String runtime, String camelVersion,
String platformBom) throws Exception {
String repos = catalogRepos.orElse(null);
+ RuntimeType runtimeType = resolveRuntime(runtime);
+
+ // If platformBom is provided (GAV format), parse and use it
+ if (platformBom != null && !platformBom.isBlank()) {
+ String[] parts = platformBom.split(":");
+ if (parts.length != 3) {
+ throw new ToolCallException(
+ "platformBom must be in GAV format
(groupId:artifactId:version), got: " + platformBom, null);
+ }
+ String groupId = parts[0];
+ String artifactId = parts[1];
+ String version = parts[2];
+
+ if (runtimeType == RuntimeType.quarkus) {
+ return CatalogLoader.loadQuarkusCatalog(repos, version,
groupId, artifactId, true);
+ } else if (runtimeType == RuntimeType.springBoot) {
+ return CatalogLoader.loadSpringBootCatalog(repos, version,
groupId, true);
+ } else {
+ return CatalogLoader.loadCatalog(repos, version, groupId,
true);
+ }
+ }
// If a specific version is requested, load that version's catalog
if (camelVersion != null && !camelVersion.isBlank()) {
- RuntimeType runtimeType = resolveRuntime(runtime);
if (runtimeType == RuntimeType.springBoot) {
return CatalogLoader.loadSpringBootCatalog(repos,
camelVersion, true);
} else if (runtimeType == RuntimeType.quarkus) {
@@ -327,7 +355,6 @@ public class CatalogTools {
}
// No specific version, use runtime-specific catalog or default
- RuntimeType runtimeType = resolveRuntime(runtime);
if (runtimeType == RuntimeType.springBoot) {
return CatalogLoader.loadSpringBootCatalog(repos, null, true);
} else if (runtimeType == RuntimeType.quarkus) {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java
index 1a7283cac144..38c5d87b4c4f 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java
@@ -18,12 +18,18 @@ package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.util.Optional;
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.catalog.DefaultCamelCatalog;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CatalogToolsTest {
+ private static final String BUILTIN_VERSION = new
DefaultCamelCatalog().getCatalogVersion();
+
private CatalogTools createTools(String repos) {
CatalogTools tools = new CatalogTools();
tools.catalogRepos = Optional.ofNullable(repos);
@@ -34,8 +40,7 @@ class CatalogToolsTest {
void defaultCatalogWithNoRepos() {
CatalogTools tools = createTools(null);
- // Should use the default catalog (no version specified, no repos)
- CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null);
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, null);
assertThat(result).isNotNull();
assertThat(result.camelVersion()).isNotNull();
@@ -45,7 +50,7 @@ class CatalogToolsTest {
void defaultCatalogWithEmptyRepos() {
CatalogTools tools = createTools("");
- CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null);
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, null);
assertThat(result).isNotNull();
assertThat(result.camelVersion()).isNotNull();
@@ -55,8 +60,7 @@ class CatalogToolsTest {
void catalogWithExtraRepos() {
CatalogTools tools =
createTools("https://maven.repository.redhat.com/ga/");
- // Should still work — the extra repo is added but default catalog
doesn't need it
- CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null);
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, null);
assertThat(result).isNotNull();
assertThat(result.camelVersion()).isNotNull();
@@ -67,7 +71,7 @@ class CatalogToolsTest {
CatalogTools tools = createTools(
"https://maven.repository.redhat.com/ga/,https://repository.jboss.org/nexus/content/groups/public/");
- CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null);
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, null);
assertThat(result).isNotNull();
assertThat(result.camelVersion()).isNotNull();
@@ -77,10 +81,152 @@ class CatalogToolsTest {
void componentDocWithRepos() {
CatalogTools tools =
createTools("https://maven.repository.redhat.com/ga/");
- // timer is a core component, should always be available
- CatalogTools.ComponentDetailResult result =
tools.camel_catalog_component_doc("timer", null, null);
+ CatalogTools.ComponentDetailResult result =
tools.camel_catalog_component_doc("timer", null, null, null);
assertThat(result).isNotNull();
assertThat(result.name()).isEqualTo("timer");
}
+
+ // platformBom validation tests
+
+ @Test
+ void platformBomInvalidFormatThrows() {
+ CatalogTools tools = createTools(null);
+
+ assertThatThrownBy(() -> tools.camel_catalog_components(null, null, 5,
null, null, "invalid-format"))
+ .isInstanceOf(ToolCallException.class)
+ .hasMessageContaining("GAV format");
+ }
+
+ @Test
+ void platformBomInvalidFormatTwoPartsThrows() {
+ CatalogTools tools = createTools(null);
+
+ assertThatThrownBy(() -> tools.camel_catalog_components(null, null, 5,
null, null, "group:artifact"))
+ .isInstanceOf(ToolCallException.class)
+ .hasMessageContaining("GAV format");
+ }
+
+ @Test
+ void platformBomNullDoesNotThrow() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, null);
+
+ assertThat(result).isNotNull();
+ assertThat(result.camelVersion()).isNotNull();
+ }
+
+ @Test
+ void platformBomEmptyStringDoesNotThrow() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, "");
+
+ assertThat(result).isNotNull();
+ assertThat(result.camelVersion()).isNotNull();
+ }
+
+ @Test
+ void platformBomBlankStringDoesNotThrow() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, " ");
+
+ assertThat(result).isNotNull();
+ assertThat(result.camelVersion()).isNotNull();
+ }
+
+ // Version reporting tests
+
+ @Test
+ void defaultCatalogReturnsBuiltinVersion() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, null);
+
+ assertThat(result.camelVersion()).isEqualTo(BUILTIN_VERSION);
+ }
+
+ @Test
+ void emptyVersionAndPlatformBomReturnsBuiltinVersion() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, "", "");
+
+ assertThat(result.camelVersion()).isEqualTo(BUILTIN_VERSION);
+ }
+
+ @Test
+ void specificCamelVersionReturnsRequestedVersion() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentListResult result
+ = tools.camel_catalog_components(null, null, 5, null,
BUILTIN_VERSION, null);
+
+ assertThat(result.camelVersion()).isEqualTo(BUILTIN_VERSION);
+ }
+
+ @Test
+ void platformBomVersionReturnsRequestedVersion() {
+ CatalogTools tools = createTools(null);
+
+ String bom = "org.apache.camel:camel-catalog:" + BUILTIN_VERSION;
+ CatalogTools.ComponentListResult result =
tools.camel_catalog_components(null, null, 5, null, null, bom);
+
+ assertThat(result.camelVersion()).isEqualTo(BUILTIN_VERSION);
+ }
+
+ @Test
+ void platformBomVersionTakesPrecedenceOverCamelVersion() {
+ CatalogTools tools = createTools(null);
+
+ String bom = "org.apache.camel:camel-catalog:" + BUILTIN_VERSION;
+ CatalogTools.ComponentListResult result
+ = tools.camel_catalog_components(null, null, 5, null,
"9.99.99", bom);
+
+ assertThat(result.camelVersion()).isEqualTo(BUILTIN_VERSION);
+ }
+
+ // Download tests — require Maven Central access.
+ // Disabled in CI environments to avoid flaky network-dependent failures.
+
+ @Test
+ @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
+ disabledReason = "Runs only local — requires
Maven Central access")
+ void downloadedCatalogVersionDiffersFromBuiltin() {
+ CatalogTools tools = createTools(null);
+
+ String requestedVersion = "4.10.0";
+ CatalogTools.ComponentListResult listResult
+ = tools.camel_catalog_components("timer", null, 1, "main",
requestedVersion, null);
+
+ assertThat(listResult.camelVersion()).isEqualTo(requestedVersion);
+
+ CatalogTools.ComponentDetailResult docResult
+ = tools.camel_catalog_component_doc("timer", "main",
requestedVersion, null);
+
+ assertThat(docResult.version()).isEqualTo(requestedVersion);
+ assertThat(docResult.version()).isNotEqualTo(BUILTIN_VERSION);
+ }
+
+ @Test
+ @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
+ disabledReason = "Runs only local — requires
Maven Central access")
+ void downloadedCatalogViaPlatformBomVersionDiffersFromBuiltin() {
+ CatalogTools tools = createTools(null);
+
+ String requestedVersion = "4.10.0";
+ String bom = "org.apache.camel:camel-catalog:" + requestedVersion;
+ CatalogTools.ComponentListResult listResult
+ = tools.camel_catalog_components("timer", null, 1, "main",
null, bom);
+
+ assertThat(listResult.camelVersion()).isEqualTo(requestedVersion);
+
+ CatalogTools.ComponentDetailResult docResult
+ = tools.camel_catalog_component_doc("timer", "main", null,
bom);
+
+ assertThat(docResult.version()).isEqualTo(requestedVersion);
+ assertThat(docResult.version()).isNotEqualTo(BUILTIN_VERSION);
+ }
}