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

Reply via email to