This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6604df2a95a08b1634dcab39b6bcb0ebb3ab4c08 Author: Luigi De Masi <[email protected]> AuthorDate: Fri Nov 13 15:41:21 2026 +0100 CAMEL-26926: Keep runtime catalogs on the selected Camel version Co-authored-by: Codex <[email protected]> Signed-off-by: Luigi De Masi <[email protected]> --- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 11 ++ .../core/commands/mcp/CatalogServiceTest.java | 38 ++++- .../camel/dsl/jbang/run/common/CatalogLoader.java | 72 +++++---- .../dsl/jbang/run/common/CatalogLoaderTest.java | 174 +++++++++++++++++++++ 4 files changed, 263 insertions(+), 32 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index e5a6622f8a52..275b70677b0d 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -8612,3 +8612,14 @@ component without a Vert.x, and is rejected with a clear message instead. the split pages, whose content it does not return in the response, so every content field of the reply was empty. It now fails with an `IllegalArgumentException` that names the reason and the way out, `outputFormat=html` to get the HTML of the whole document. + +=== camel-jbang - runtime catalogs use the selected Camel version + +Spring Boot and Quarkus catalogs now report the selected Camel core version and read EIP/model schemas from that +version's catalog. Previously, they could combine the selected runtime's component schemas with the running CLI or +MCP server's newer model schemas. A model absent from the selected catalog now remains absent. +Runtime dependency coordinates still use the selected Spring Boot or Camel Quarkus version. + +A Quarkus BOM must identify both `org.apache.camel:camel-catalog` and +`org.apache.camel.quarkus:camel-quarkus-catalog`. If either version or the selected catalog cannot be resolved, +catalog loading fails instead of returning the running server's catalog. diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java index a6714a548d12..d7da5d276f03 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java @@ -203,10 +203,44 @@ class CatalogServiceTest { // extension registry and pass it to doLoadCatalog; previously it passed null and produced an NPE or a // silently wrong catalog. Use a released version that is not the built-in one so sameAsDefault() is false // and the new code path is taken. - // Note: getCatalogVersion() returns the CLI's own Camel version (from camel-catalog on the classpath), - // not the requested "4.10.0". Check the runtime provider instead to verify the Quarkus path was taken. CatalogService service = createService(null); CamelCatalog catalog = service.loadCatalog("quarkus", "4.10.0", null); assertThat(catalog.getRuntimeProvider().getProviderName()).isEqualTo("quarkus"); } + + @Test + @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", + disabledReason = "Runs only local — requires Maven Central access") + void mixedRuntimeRequestsKeepTheirCoreSchemasAndProviderCoordinates() throws Exception { + CatalogService service = createService(null); + String targetSplit = service.loadCatalog("main", "4.8.0", null).modelJSonSchema("split"); + ConfigurationValidateTools tools = new ConfigurationValidateTools(); + tools.catalogService = service; + String[][] requests = { + { "spring-boot", "4.8.0", null, "camel-http-starter", "4.8.0" }, + { "quarkus", null, "io.quarkus.platform:quarkus-camel-bom:3.15.1", "camel-quarkus-http", "3.15.0" }, + { "main", "4.8.0", null, "camel-http", "4.8.0" }, + { + "spring-boot", "4.8.0", "org.apache.camel.springboot:camel-spring-boot-bom:4.8.0", + "camel-http-starter", "4.8.0" } + }; + for (int pass = 0; pass < 2; pass++) { + for (String[] request : requests) { + CamelCatalog catalog = service.loadCatalog(request[0], request[1], request[2]); + assertThat(catalog.getCatalogVersion()).isEqualTo("4.8.0"); + assertThat(catalog.modelJSonSchema("split")).isEqualTo(targetSplit).doesNotContain("errorThreshold"); + assertThat(catalog.modelJSonSchema("switch")).isNull(); + assertThat(catalog.componentModel("http").getArtifactId()).isEqualTo(request[3]); + assertThat(catalog.componentModel("http").getVersion()).isEqualTo(request[4]); + var result = tools.camel_configuration_validate("camel.component.http.max-total-connections=20", + request[0], request[1], request[2]); + assertThat(result.camelVersion()).isEqualTo("4.8.0"); + assertThat(result.summary().valid()).isEqualTo(1); + } + CamelCatalog current = service.loadCatalog("main", BUILTIN_VERSION, null); + assertThat(current.getCatalogVersion()).isEqualTo(BUILTIN_VERSION); + assertThat(current.modelJSonSchema("split")).contains("errorThreshold"); + assertThat(current.modelJSonSchema("switch")).isNotNull(); + } + } } diff --git a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/CatalogLoader.java b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/CatalogLoader.java index 65e6fcf336c3..5391edc55674 100644 --- a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/CatalogLoader.java +++ b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/CatalogLoader.java @@ -45,8 +45,6 @@ import org.apache.camel.tooling.maven.MavenGav; public final class CatalogLoader { - private static final String DEFAULT_CAMEL_CATALOG = "org.apache.camel.catalog.DefaultCamelCatalog"; - private static final String SPRING_BOOT_CATALOG_PROVIDER = "org.apache.camel.springboot.catalog.SpringBootRuntimeProvider"; private static final String QUARKUS_CATALOG_PROVIDER = "org.apache.camel.catalog.quarkus.QuarkusRuntimeProvider"; @@ -141,6 +139,8 @@ public final class CatalogLoader { } DependencyDownloaderClassLoader cl = new DependencyDownloaderClassLoader(CatalogLoader.class.getClassLoader()); + // Provider classes share the running catalog API; resources must come only from the selected artifacts. + DependencyDownloaderClassLoader resources = new DependencyDownloaderClassLoader(null); MavenDependencyDownloader downloader = new MavenDependencyDownloader(); downloader.setClassLoader(cl); downloader.setRepositories(repos); @@ -158,7 +158,7 @@ public final class CatalogLoader { ma = downloader.downloadArtifact("org.apache.camel", "camel-catalog", camelCatalogVersion); } if (ma != null) { - cl.addFile(ma.getFile()); + resources.addFile(ma.getFile()); } else { throw new IOException("Cannot download org.apache.camel:camel-catalog:" + camelCatalogVersion); } @@ -167,6 +167,7 @@ public final class CatalogLoader { ma = downloader.downloadArtifact(springBootGroupId, "camel-catalog-provider-springboot", sbVersion); if (ma != null) { cl.addFile(ma.getFile()); + resources.addFile(ma.getFile()); } else { throw new IOException( "Cannot download " + springBootGroupId + ":camel-catalog-provider-springboot:" + version); @@ -183,16 +184,11 @@ public final class CatalogLoader { + " Use --camel-spring-boot-version=4.18.x for JDK 17 compatibility, or run on JDK 21+."); } if (clazz != null) { - Class<CamelCatalog> clazz2 = (Class<CamelCatalog>) cl.loadClass(DEFAULT_CAMEL_CATALOG); - if (clazz2 != null) { - answer = ObjectHelper.newInstance(clazz2); - } RuntimeProvider provider = ObjectHelper.newInstance(clazz); if (provider != null) { answer.setRuntimeProvider(provider); } - // use classloader that loaded spring-boot provider to ensure we can load its resources - answer.getVersionManager().setClassLoader(cl); + answer.setVersionManager(new DownloadCatalogVersionManager(camelCatalogVersion, sbVersion, resources)); } answer.enableCache(); } finally { @@ -206,6 +202,7 @@ public final class CatalogLoader { public static CamelCatalog loadQuarkusCatalog(MavenGav quarkusCamelBom, Function<MavenGav, MavenArtifact> downloader) throws Exception { String camelQuarkusVersion = null; + String camelVersion = null; CamelCatalog answer = new DefaultCamelCatalog(); // shrinkwrap does not return POM file as result (they are hardcoded to be filtered out) @@ -227,32 +224,41 @@ public final class CatalogLoader { String a = node.getElementsByTagName("artifactId").item(0).getTextContent(); if ("org.apache.camel.quarkus".equals(g) && "camel-quarkus-catalog".equals(a)) { camelQuarkusVersion = node.getElementsByTagName("version").item(0).getTextContent(); - break; + } else if ("org.apache.camel".equals(g) && "camel-catalog".equals(a)) { + camelVersion = node.getElementsByTagName("version").item(0).getTextContent(); } } } } - if (camelQuarkusVersion != null) { - // download camel-quarkus-catalog we use to know if we have an extension or not - MavenArtifact artifact = downloader.apply(MavenGav.fromCoordinates("org.apache.camel.quarkus", - "camel-quarkus-catalog", camelQuarkusVersion, "jar", null)); - DependencyDownloaderClassLoader cl - = new DependencyDownloaderClassLoader(Thread.currentThread().getContextClassLoader()); - cl.addFile(artifact.getFile()); - Class<RuntimeProvider> clazz = (Class<RuntimeProvider>) cl.loadClass(QUARKUS_CATALOG_PROVIDER); - if (clazz != null) { - Class<CamelCatalog> clazz2 = (Class<CamelCatalog>) cl.loadClass(DEFAULT_CAMEL_CATALOG); - if (clazz2 != null) { - answer = ObjectHelper.newInstance(clazz2); - } - RuntimeProvider provider = ObjectHelper.newInstance(clazz); - if (provider != null) { - answer.setRuntimeProvider(provider); - } - // use classloader that loaded quarkus provider to ensure we can load its resources - answer.getVersionManager().setClassLoader(cl); + if (camelQuarkusVersion == null || camelVersion == null) { + throw new IOException("Cannot determine Camel and Camel Quarkus catalog versions from " + quarkusCamelBom); + } + + // download camel-quarkus-catalog we use to know if we have an extension or not + MavenArtifact artifact = downloader.apply(MavenGav.fromCoordinates("org.apache.camel.quarkus", + "camel-quarkus-catalog", camelQuarkusVersion, "jar", null)); + DependencyDownloaderClassLoader cl + = new DependencyDownloaderClassLoader(Thread.currentThread().getContextClassLoader()); + if (artifact == null || artifact.getFile() == null) { + throw new IOException("Cannot download org.apache.camel.quarkus:camel-quarkus-catalog:" + camelQuarkusVersion); + } + cl.addFile(artifact.getFile()); + DependencyDownloaderClassLoader resources = new DependencyDownloaderClassLoader(null); + MavenArtifact coreCatalog + = downloader.apply(MavenGav.fromCoordinates("org.apache.camel", "camel-catalog", camelVersion, "jar", null)); + if (coreCatalog == null || coreCatalog.getFile() == null) { + throw new IOException("Cannot download org.apache.camel:camel-catalog:" + camelVersion); + } + resources.addFile(coreCatalog.getFile()); + resources.addFile(artifact.getFile()); + Class<RuntimeProvider> clazz = (Class<RuntimeProvider>) cl.loadClass(QUARKUS_CATALOG_PROVIDER); + if (clazz != null) { + RuntimeProvider provider = ObjectHelper.newInstance(clazz); + if (provider != null) { + answer.setRuntimeProvider(provider); } + answer.setVersionManager(new DownloadCatalogVersionManager(camelVersion, camelQuarkusVersion, resources)); } answer.enableCache(); @@ -365,9 +371,15 @@ public final class CatalogLoader { private ClassLoader classLoader; private final String version; + private final String providerVersion; public DownloadCatalogVersionManager(String version, ClassLoader classLoader) { + this(version, version, classLoader); + } + + public DownloadCatalogVersionManager(String version, String providerVersion, ClassLoader classLoader) { this.version = version; + this.providerVersion = providerVersion; this.classLoader = classLoader; } @@ -393,7 +405,7 @@ public final class CatalogLoader { @Override public String getRuntimeProviderLoadedVersion() { - return version; + return providerVersion; } @Override diff --git a/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/common/CatalogLoaderTest.java b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/common/CatalogLoaderTest.java new file mode 100644 index 000000000000..31642a1d99de --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/common/CatalogLoaderTest.java @@ -0,0 +1,174 @@ +/* + * 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.run.common; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipFile; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.maven.MavenArtifact; +import org.apache.camel.tooling.maven.MavenDownloader; +import org.apache.camel.tooling.maven.MavenDownloaderImpl; +import org.apache.camel.tooling.maven.MavenGav; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CatalogLoaderTest { + + private static final String CAMEL_VERSION = "4.8.0"; + private static MavenDownloader downloader; + private static MavenArtifact coreCatalog; + + @BeforeAll + static void downloadCatalog() { + downloader = new MavenDownloaderImpl(); + downloader.build(); + coreCatalog = artifact("org.apache.camel", "camel-catalog", CAMEL_VERSION); + } + + @AfterAll + static void closeDownloader() throws Exception { + if (downloader != null) { + downloader.stop(); + } + } + + @Test + void mainCatalogUsesTargetResources() throws Exception { + CamelCatalog catalog = CatalogLoader.loadCatalog(null, CAMEL_VERSION, true); + assertCoreCatalog(catalog); + assertProvider(catalog, coreCatalog, "org/apache/camel/catalog", "org.apache.camel", "camel-http", CAMEL_VERSION); + } + + @Test + void springBootCatalogUsesTargetResources() throws Exception { + CamelCatalog catalog = CatalogLoader.loadSpringBootCatalog(null, CAMEL_VERSION, true); + assertCoreCatalog(catalog); + assertProvider(catalog, artifact("org.apache.camel.springboot", "camel-catalog-provider-springboot", CAMEL_VERSION), + "org/apache/camel/springboot/catalog", "org.apache.camel.springboot", "camel-http-starter", CAMEL_VERSION); + assertThat(catalog.getRuntimeProviderLoadedVersion()).isEqualTo(CAMEL_VERSION); + } + + @Test + void quarkusCatalogUsesBomCoreAndProviderVersions() throws Exception { + CamelCatalog catalog = CatalogLoader.loadQuarkusCatalog( + MavenGav.fromCoordinates("io.quarkus.platform", "quarkus-camel-bom", "3.15.1", "pom", null), + downloader::resolveArtifact); + assertCoreCatalog(catalog); + assertProvider(catalog, artifact("org.apache.camel.quarkus", "camel-quarkus-catalog", "3.15.0"), + "org/apache/camel/catalog/quarkus", "org.apache.camel.quarkus", "camel-quarkus-http", "3.15.0"); + assertThat(catalog.getRuntimeProviderLoadedVersion()).isEqualTo("3.15.0"); + } + + @Test + void springBootProviderVersionCanDifferFromCore() throws Exception { + CamelCatalog current = new DefaultCamelCatalog(); + CamelCatalog catalog + = CatalogLoader.loadSpringBootCatalog(null, current.getCatalogVersion(), CAMEL_VERSION, null, true); + assertThat(catalog.getCatalogVersion()).isEqualTo(current.getCatalogVersion()); + assertThat(catalog.modelJSonSchema("split")).isEqualTo(current.modelJSonSchema("split")); + assertThat(catalog.getRuntimeProviderLoadedVersion()).isEqualTo(CAMEL_VERSION); + assertProvider(catalog, artifact("org.apache.camel.springboot", "camel-catalog-provider-springboot", CAMEL_VERSION), + "org/apache/camel/springboot/catalog", "org.apache.camel.springboot", "camel-http-starter", CAMEL_VERSION); + } + + @Test + void quarkusBomWithoutCoreCatalogVersionFails(@TempDir Path directory) throws Exception { + Path bom = directory.resolve("bom.pom"); + Files.writeString(bom, """ + <project><dependencyManagement><dependencies><dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-catalog</artifactId><version>3.15.0</version> + </dependency></dependencies></dependencyManagement></project> + """); + MavenGav gav = MavenGav.fromCoordinates("test", "bom", "1", "pom", null); + assertThatThrownBy(() -> CatalogLoader.loadQuarkusCatalog(gav, requested -> { + assertThat(requested).isEqualTo(gav); + return new MavenArtifact(requested, bom.toFile()); + })).isInstanceOf(IOException.class).hasMessageContaining("Cannot determine Camel and Camel Quarkus catalog versions"); + } + + @Test + void quarkusMissingCoreCatalogFails() { + MavenGav bom = MavenGav.fromCoordinates("io.quarkus.platform", "quarkus-camel-bom", "3.15.1", "pom", null); + assertThatThrownBy(() -> CatalogLoader.loadQuarkusCatalog(bom, gav -> { + if ("org.apache.camel".equals(gav.getGroupId()) && "camel-catalog".equals(gav.getArtifactId())) { + return null; + } + return downloader.resolveArtifact(gav); + })).isInstanceOf(IOException.class).hasMessageContaining("Cannot download org.apache.camel:camel-catalog:4.8.0"); + } + + private static MavenArtifact artifact(String groupId, String artifactId, String version) { + return downloader.resolveArtifact(MavenGav.fromCoordinates(groupId, artifactId, version, "jar", null)); + } + + private static void assertCoreCatalog(CamelCatalog catalog) throws IOException { + assertThat(catalog.getCatalogVersion()).isEqualTo(CAMEL_VERSION); + assertThat(new DefaultCamelCatalog().modelJSonSchema("split")).contains("errorThreshold"); + assertThat(catalog.modelJSonSchema("split")).doesNotContain("errorThreshold"); + try (ZipFile jar = new ZipFile(coreCatalog.getFile())) { + for (String name : catalog.findModelNames()) { + assertThat(catalog.modelJSonSchema(name)).as("model %s", name) + .isEqualTo(resource(jar, "org/apache/camel/catalog/models/" + name + ".json")); + } + } + assertThat(new DefaultCamelCatalog().modelJSonSchema("switch")).isNotNull(); + assertThat(catalog.modelJSonSchema("switch")).isNull(); + assertThat(catalog.findModelNames()).doesNotContain("switch"); + } + + private static void assertProvider( + CamelCatalog catalog, MavenArtifact artifact, String resourcePath, String groupId, String artifactId, + String version) + throws IOException { + try (ZipFile jar = new ZipFile(artifact.getFile())) { + for (String name : catalog.findComponentNames()) { + assertThat(catalog.componentJSonSchema(name)).as("component %s", name) + .isEqualTo(resource(jar, resourcePath + "/components/" + name + ".json")); + } + for (String name : catalog.findDataFormatNames()) { + assertThat(catalog.dataFormatJSonSchema(name)).as("data format %s", name) + .isEqualTo(resource(jar, resourcePath + "/dataformats/" + name + ".json")); + } + for (String name : catalog.findLanguageNames()) { + assertThat(catalog.languageJSonSchema(name)).as("language %s", name) + .isEqualTo(resource(jar, resourcePath + "/languages/" + name + ".json")); + } + } + assertThat(catalog.componentModel("http").getGroupId()).isEqualTo(groupId); + assertThat(catalog.componentModel("http").getArtifactId()).isEqualTo(artifactId); + assertThat(catalog.componentModel("http").getVersion()).isEqualTo(version); + } + + private static String resource(ZipFile jar, String name) throws IOException { + assertThat(jar.getEntry(name)).as("target resource %s", name).isNotNull(); + try (InputStream stream = jar.getInputStream(jar.getEntry(name))) { + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + } +}
