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 b7d3bd2a5b336cc01dc75bae1c9de8d0ff75fe1f Author: Luigi De Masi <[email protected]> AuthorDate: Fri Nov 13 17:43:12 2026 +0100 CAMEL-26926: Address catalog loading review feedback Reject absent and blank BOM catalog versions with actionable errors. Cover Spring Boot export versions and retain explicit core overrides. Document the retained fallback and isolate tests from global properties. Co-authored-by: Codex <[email protected]> Signed-off-by: Luigi De Masi <[email protected]> --- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 30 +++++--- .../core/commands/mcp/CatalogServiceTest.java | 19 +++-- .../camel/dsl/jbang/run/common/CatalogLoader.java | 30 ++++---- .../dsl/jbang/run/common/CatalogLoaderBomTest.java | 81 ++++++++++++++++++++++ .../dsl/jbang/run/common/CatalogLoaderTest.java | 22 +----- .../dsl/jbang/run/export/ProjectExporterTest.java | 38 ++++++++++ 6 files changed, 168 insertions(+), 52 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 275b70677b0d..08bd081d00ce 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 @@ -8067,6 +8067,25 @@ dead letter channel or a handled `onException`, so that the item is marked consu `delay`, or set `backoffIdleThreshold` and `backoffMultiplier`: a poll counts only the items that it marked consumed, so a poll whose items all failed is an idle poll, after which a `greedy` consumer does not poll again at once. +=== camel-jbang - runtime catalogs use the selected Camel version + +Spring Boot and Quarkus catalogs now report the resolved 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. + +Spring Boot retains its existing fallback: if downloading `org.apache.camel:camel-catalog` for the requested +version fails, it tries the running CLI's Camel version. The catalog reports the core version actually resolved. + +For Spring Boot, `camel export` and `camel run` without an explicit `--camel-version` now use the resolved core +catalog version for Camel core dependencies in the generated project. For example, `--camel-spring-boot-version=4.8.0` +uses Camel 4.8.0 instead of the running CLI's version when that catalog is available. Set `--camel-version` explicitly +to retain a different core dependency version. + === camel-jbang - the Quarkus Platform is looked up only for the Quarkus runtime With a runtime other than Quarkus, `camel run` and `camel export` no longer look up the Quarkus Platform in the Quarkus @@ -8612,14 +8631,3 @@ 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 d7da5d276f03..6bd80d5bd6f4 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 @@ -226,16 +226,21 @@ class CatalogServiceTest { }; for (int pass = 0; pass < 2; pass++) { for (String[] request : requests) { + String scenario = "runtime=" + request[0] + ", camelVersion=" + request[1] + + ", platformBom=" + request[2] + ", pass=" + pass; 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]); + assertThat(catalog.getCatalogVersion()).as("%s: core version", scenario).isEqualTo("4.8.0"); + assertThat(catalog.modelJSonSchema("split")).as("%s: split schema", scenario).isEqualTo(targetSplit) + .doesNotContain("errorThreshold"); + assertThat(catalog.modelJSonSchema("switch")).as("%s: missing switch schema", scenario).isNull(); + assertThat(catalog.componentModel("http").getArtifactId()).as("%s: provider artifact", scenario) + .isEqualTo(request[3]); + assertThat(catalog.componentModel("http").getVersion()).as("%s: provider version", scenario) + .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); + assertThat(result.camelVersion()).as("%s: validation version", scenario).isEqualTo("4.8.0"); + assertThat(result.summary().valid()).as("%s: valid configurations", scenario).isEqualTo(1); } CamelCatalog current = service.loadCatalog("main", BUILTIN_VERSION, null); assertThat(current.getCatalogVersion()).isEqualTo(BUILTIN_VERSION); 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 5391edc55674..7cb1acb0ba7d 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 @@ -30,6 +30,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.apache.camel.catalog.CamelCatalog; @@ -170,7 +171,7 @@ public final class CatalogLoader { resources.addFile(ma.getFile()); } else { throw new IOException( - "Cannot download " + springBootGroupId + ":camel-catalog-provider-springboot:" + version); + "Cannot download " + springBootGroupId + ":camel-catalog-provider-springboot:" + sbVersion); } Class<RuntimeProvider> clazz; @@ -223,26 +224,31 @@ public final class CatalogLoader { String g = node.getElementsByTagName("groupId").item(0).getTextContent(); 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(); + Node version = node.getElementsByTagName("version").item(0); + camelQuarkusVersion = version != null ? version.getTextContent().trim() : null; } else if ("org.apache.camel".equals(g) && "camel-catalog".equals(a)) { - camelVersion = node.getElementsByTagName("version").item(0).getTextContent(); + Node version = node.getElementsByTagName("version").item(0); + camelVersion = version != null ? version.getTextContent().trim() : null; } } } } - if (camelQuarkusVersion == null || camelVersion == null) { - throw new IOException("Cannot determine Camel and Camel Quarkus catalog versions from " + quarkusCamelBom); + if (camelQuarkusVersion == null || camelQuarkusVersion.isEmpty() || camelVersion == null || camelVersion.isEmpty()) { + throw new IOException( + "Cannot determine Camel and Camel Quarkus catalog versions from " + quarkusCamelBom + + "; expected org.apache.camel:camel-catalog and org.apache.camel.quarkus:camel-quarkus-catalog" + + " dependencies with non-empty versions"); } // 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); } + DependencyDownloaderClassLoader cl + = new DependencyDownloaderClassLoader(Thread.currentThread().getContextClassLoader()); cl.addFile(artifact.getFile()); DependencyDownloaderClassLoader resources = new DependencyDownloaderClassLoader(null); MavenArtifact coreCatalog @@ -253,13 +259,9 @@ public final class CatalogLoader { 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)); - } + RuntimeProvider provider = ObjectHelper.newInstance(clazz); + answer.setRuntimeProvider(provider); + answer.setVersionManager(new DownloadCatalogVersionManager(camelVersion, camelQuarkusVersion, resources)); answer.enableCache(); return answer; diff --git a/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/common/CatalogLoaderBomTest.java b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/common/CatalogLoaderBomTest.java new file mode 100644 index 000000000000..a7ff68ce00f7 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/common/CatalogLoaderBomTest.java @@ -0,0 +1,81 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; + +import org.apache.camel.tooling.maven.MavenArtifact; +import org.apache.camel.tooling.maven.MavenGav; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CatalogLoaderBomTest { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + void rejectsMissingOrBlankCatalogVersions(String version, @TempDir Path directory) throws Exception { + String versionElement = version == null ? "" : "<version>" + version + "</version>"; + for (boolean core : new boolean[] { true, false }) { + Path bom = directory.resolve("bom.pom"); + Files.writeString(bom, + """ + <project><dependencyManagement><dependencies> + <dependency><groupId>org.apache.camel</groupId><artifactId>camel-catalog</artifactId>%s</dependency> + <dependency><groupId>org.apache.camel.quarkus</groupId><artifactId>camel-quarkus-catalog</artifactId>%s</dependency> + </dependencies></dependencyManagement></project> + """ + .formatted(core ? versionElement : "<version>4.8.0</version>", + core ? "<version>3.15.0</version>" : versionElement)); + MavenGav gav = MavenGav.fromCoordinates("test", "bom", "1", "pom", null); + assertThatThrownBy(() -> CatalogLoader.loadQuarkusCatalog(gav, requested -> { + assertThat(requested).as("only the BOM should be requested").isEqualTo(gav); + return new MavenArtifact(requested, bom.toFile()); + })).as("invalid %s catalog version: [%s]", core ? "core" : "Quarkus", version) + .isInstanceOf(IOException.class) + .hasMessageContaining("Cannot determine Camel and Camel Quarkus catalog versions") + .hasMessageContaining(gav.toString()) + .hasMessageContaining("org.apache.camel:camel-catalog") + .hasMessageContaining("org.apache.camel.quarkus:camel-quarkus-catalog"); + } + } + + @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"); + } + +} 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 index 31642a1d99de..af4560e26f03 100644 --- 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 @@ -19,8 +19,6 @@ 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; @@ -32,7 +30,6 @@ 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; @@ -43,6 +40,7 @@ class CatalogLoaderTest { private static MavenDownloader downloader; private static MavenArtifact coreCatalog; + // These released-artifact comparisons deliberately run in CI to catch catalog resource mixing. @BeforeAll static void downloadCatalog() { downloader = new MavenDownloaderImpl(); @@ -66,7 +64,7 @@ class CatalogLoaderTest { @Test void springBootCatalogUsesTargetResources() throws Exception { - CamelCatalog catalog = CatalogLoader.loadSpringBootCatalog(null, CAMEL_VERSION, true); + CamelCatalog catalog = CatalogLoader.loadSpringBootCatalog(null, CAMEL_VERSION, CAMEL_VERSION, null, 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); @@ -96,22 +94,6 @@ class CatalogLoaderTest { "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); diff --git a/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/export/ProjectExporterTest.java b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/export/ProjectExporterTest.java index 7d6d02463f0c..cf83478dbb90 100644 --- a/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/export/ProjectExporterTest.java +++ b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/export/ProjectExporterTest.java @@ -30,6 +30,8 @@ import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.stream.Stream; +import javax.xml.xpath.XPathFactory; + import org.apache.camel.dsl.jbang.run.common.CamelHome; import org.apache.camel.dsl.jbang.run.common.ExportHelper; import org.apache.camel.dsl.jbang.run.common.MavenResolver; @@ -37,12 +39,15 @@ import org.apache.camel.dsl.jbang.run.common.PathUtils; import org.apache.camel.dsl.jbang.run.common.QuarkusPlatform; import org.apache.camel.dsl.jbang.run.common.RuntimeType; import org.apache.camel.dsl.jbang.run.common.StringPrinter; +import org.apache.camel.dsl.jbang.run.common.XmlHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.SetSystemProperty; import static org.assertj.core.api.Assertions.assertThat; @@ -110,6 +115,39 @@ class ProjectExporterTest { assertThat(request.workDir.resolve(ExportHelper.RUN_SETTINGS_FILE)).exists(); } + @ParameterizedTest + @NullSource + @ValueSource(strings = "4.10.0") + void springBootExportUsesResolvedCoreVersionUnlessExplicit(String camelVersion) throws Exception { + Path exportDir = dir.resolve("export"); + ExportRequest request = request(writeRoute("hello"), exportDir); + request.runtime = RuntimeType.springBoot; + request.camelSpringBootVersion = "4.8.0"; + request.camelVersion = camelVersion; + DependencyResolver withCoreDependency = r -> { + resolver.resolve(r); + Path settings = r.workDir.resolve(ExportHelper.RUN_SETTINGS_FILE); + Files.writeString(settings, Files.readString(settings) + "dependency=camel:util\n"); + return 0; + }; + StringPrinter printer = new StringPrinter(); + int exit = new ProjectExporter( + new ExportContext().withPrinter(printer) + .withDependencyResolver(withCoreDependency)) + .export(request); + + assertThat(exit).as(printer.getOutput()).isZero(); + var factory = XmlHelper.createDocumentBuilderFactory(); + factory.setNamespaceAware(false); + var pom = factory.newDocumentBuilder().parse(exportDir.resolve("pom.xml").toFile()); + var xpath = XPathFactory.newInstance().newXPath(); + assertThat(xpath.evaluate("/project/dependencies/dependency[artifactId='camel-util']/version", pom)) + .isEqualTo(camelVersion == null ? "4.8.0" : camelVersion); + assertThat(xpath.evaluate( + "/project/dependencyManagement/dependencies/dependency[artifactId='camel-spring-boot-bom']/version", pom)) + .isEqualTo("4.8.0"); + } + @Test void keepsTheKeysOfTheApplicationPropertiesThatTheRunSettingsRepeat() throws Exception { Path route = writeRoute("shop");
