This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 e0855013fbdb CAMEL-25642: camel-jbang-mcp - migration and dependency
tools answer with the right data (#27959)
e0855013fbdb is described below
commit e0855013fbdbfc3e7e811bf0b82415d8459d67a8
Author: Federico Mariani <[email protected]>
AuthorDate: Sat Oct 17 12:36:28 2026 +0200
CAMEL-25642: camel-jbang-mcp - migration and dependency tools answer with
the right data (#27959)
camel_migration_recipes: the Spring Boot command activates
CamelSpringBootMigrationRecipe (the recipe camel-spring-boot-upgrade-recipes
ships), the recipes artifact is the <major>.<minor>.0 release of the target,
the rewrite-maven-plugin is not pinned (as in the recipes README), and the
Quarkus command runs quarkus-maven-plugin at a Quarkus platform version
(new quarkusPlatformVersion argument, else the latest) with -Drewrite=true
for a full run instead of the unknown -DrewriteFullRun.
camel_migration_compatibility: the Java requirement comes from the JDK
versions of the release in the catalog instead of "4.19+ requires Java 21";
the unused camelComponents argument is removed.
camel_dependency_check / camel_dependency_security_audit /
camel_migration_analyze: Spring Boot starters and Camel Quarkus extensions
count as their component (mapping shared with DependencyLookup), components
only in dependencyManagement or plugin dependencies are not declared, the
camel-quarkus-bom version is not taken as the Camel version nor recommended
with it, the version is compared with the latest release instead of the
SNAPSHOT catalog version, and the audit says when the Camel version is not
detected, reports advisories without components as uncheckedAdvisories (not
clean), and leaves reachable unset without routes.
Runtime detection: a WAR is WildFly only without Spring Boot or Quarkus, and
the war, bundle and WildFly plugins are read from <plugin> elements.
camel_properties_translate: the Spring Boot only options keep their Spring
Boot key (camel.springboot.lenient-configuration-binding) and have no
equivalent on the other runtimes; camel.server.* is camel-main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 23 +++
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 39 +++--
.../jbang/core/commands/ai/DependencyLookup.java | 47 ++++-
.../dsl/jbang/core/commands/mcp/CamelReleases.java | 126 +++++++++++++
.../core/commands/mcp/DependencyCheckTools.java | 86 +++++----
.../jbang/core/commands/mcp/DependencyData.java | 51 +++++-
.../commands/mcp/DependencySecurityAuditTools.java | 166 ++++++++++++------
.../dsl/jbang/core/commands/mcp/MigrationData.java | 195 ++++++++++++++++-----
.../jbang/core/commands/mcp/MigrationTools.java | 189 ++++++++++++--------
.../jbang/core/commands/mcp/PromptDefinitions.java | 8 +-
.../commands/mcp/PropertiesTranslateTools.java | 68 ++++++-
.../jbang/core/commands/mcp/CamelReleasesTest.java | 60 +++++++
.../commands/mcp/DependencyCheckToolsTest.java | 99 ++++++++++-
.../mcp/DependencySecurityAuditToolsTest.java | 55 +++++-
.../core/commands/mcp/MigrationToolsTest.java | 189 ++++++++++++++++++++
.../core/commands/mcp/PromptDefinitionsTest.java | 4 +
.../commands/mcp/PropertiesTranslateToolsTest.java | 16 ++
17 files changed, 1180 insertions(+), 241 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 8077be87d3b3..83252eab6082 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
@@ -686,6 +686,29 @@ such as `+${body} == "hello"+`, and a slash (as in every
XPath) or a quote in th
* `get_route_analysis`, `get_eip_stats` and `detect_config_drift` have the
answers of the integration as JSON, instead
of as JSON text inside their JSON.
+=== camel-jbang-mcp - migration and dependency tools
+
+The `camel_migration_compatibility` tool no longer takes the `camelComponents`
argument, which it never used. It
+now takes the Java requirement from the JDK versions of the target release in
the catalog (the same data as
+`camel_version_list`): Camel 4.19 and later no longer report Java 17 as a
blocker, and `javaCompatibility` also
+lists the `supportedVersions`. A target version newer than every release in
the catalog has no Java requirement: the
+tool says so in `warnings` instead of taking the one of the newest release.
+
+`camel_migration_recipes` returns commands that run: the Spring Boot command
activates
+`org.apache.camel.upgrade.CamelSpringBootMigrationRecipe`, the recipes
artifact is the `<major>.<minor>.0` release
+of the target version, the OpenRewrite plugin is no longer pinned to a
version, and the Quarkus command runs the
+`quarkus-maven-plugin` at the Quarkus platform version of the new optional
`quarkusPlatformVersion` argument (or the
+latest one), with `-Drewrite=true` for a full run.
+
+In `camel_dependency_check` and `camel_dependency_security_audit` the
dependencies are the declared ones: a
+component only in `dependencyManagement` or in the dependencies of a plugin no
longer counts, and Spring Boot starters
+and Camel Quarkus extensions count as the component they provide. The version
status of `camel_dependency_check`
+compares the project with the latest Camel release, added as `latestVersion`,
instead of the catalog version of the
+server. The security audit no longer audits the server's catalog version when
the `pom.xml` sets no Camel version:
+it audits the `camelVersion` argument, else lists every advisory of the
declared components, and says so in
+`warnings`. Its `reachable` is null when no routes are given, advisories
without components are returned as
+`uncheckedAdvisories`, and `clean` is true only when none of them apply.
+
=== camel-jpa, camel-hazelcast - an exchange marked rollback only is not
consumed
When a route marks the exchange rollback only (for example with
`markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index ad226858c497..4be96356681e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -623,8 +623,12 @@ client to the next.
| `camel_dependency_security_audit`
| Audits a project's dependencies for known vulnerabilities: cross-references
the `pom.xml` with the Camel
security advisory database for the CVEs affecting each artifact at the
project's Camel version, and reports
- severity, affected and fixed version ranges, and whether the vulnerable
component is actually used (reachable)
- or only a transitive dependency. The POM content is sanitized to mask
sensitive data.
+ severity, affected and fixed version ranges, and, when routes are given,
whether the vulnerable component is
+ actually used by them (reachable). Spring Boot starters and Camel Quarkus
extensions are audited as the component
+ they provide. When the `pom.xml` does not set the Camel version
(parent-managed), the answer says so and audits
+ the `camelVersion` argument, or without it lists every advisory of the
declared components. Advisories that name
+ no component cannot be matched to a dependency and are listed as
`uncheckedAdvisories`; the audit is only
+ reported `clean` when there are none. The POM content is sanitized to mask
sensitive data.
|===
==== Security advisory data
@@ -660,9 +664,11 @@ The advisories are also browseable as MCP resources:
`camel://security/advisorie
| `camel_dependency_check`
| Checks Camel project dependency hygiene given a `pom.xml` and optional route
definitions. Detects outdated
- Camel versions compared to the latest catalog release, identifies missing
Maven dependencies for components
- used in routes, and flags version conflicts between the Camel BOM and
explicit dependency overrides.
- Returns actionable recommendations with corrected dependency snippets.
+ Camel versions compared to the latest Camel release, identifies missing
Maven dependencies for components
+ used in routes (a Spring Boot starter or Camel Quarkus extension counts as
its component, and the snippet uses
+ the project's runtime artifact; a component only in `dependencyManagement`
is not declared), and flags version
+ conflicts between the Camel BOM and explicit dependency overrides. Returns
actionable recommendations with
+ corrected dependency snippets.
|===
=== Validation and Transformation
@@ -683,7 +689,10 @@ The advisories are also browseable as MCP resources:
`camel://security/advisorie
| `camel_properties_translate`
| Translate Camel configuration properties between runtimes (`main`,
`spring-boot`, `quarkus`). Handles
- runtime-specific keys like HTTP server and management endpoint configuration.
+ runtime-specific keys like HTTP server and management endpoint configuration
(`+camel.server.*+` and
+ `+camel.management.*+` on Camel Main). Legacy `+camel.springboot.*+` keys
become `+camel.main.*+`, except the Spring
+ Boot only options, which keep their Spring Boot key
(`camel.springboot.lenient-configuration-binding`) and have
+ no equivalent on the other runtimes.
|===
=== Route Diagram
@@ -734,16 +743,22 @@ help validate, scaffold, and provide mock guidance for
that workflow.
| `camel_migration_analyze`
| Analyzes a Camel project's `pom.xml` to detect the runtime type (`main`,
`spring-boot`, `quarkus`, `wildfly`,
`karaf`), Camel version, Java version, and Camel component dependencies.
This is the first step in a migration
- workflow.
+ workflow. A WAR is `wildfly` only without Spring Boot or Quarkus, and the
dependencies are the declared ones,
+ not those in `dependencyManagement` or of a plugin.
| `camel_migration_compatibility`
-| Checks migration compatibility for Camel components by providing relevant
migration guide URLs and Java version
- requirements. The LLM consults the migration guides for detailed component
rename mappings and API changes.
+| Checks whether a migration between two Camel versions is possible on the
project's Java version, from the JDK
+ versions each release supports in the catalog (as `camel_version_list`
reports them), and returns the relevant
+ migration guide URLs. The LLM consults the migration guides for detailed
component rename mappings and API
+ changes.
| `camel_migration_recipes`
-| Returns Maven commands to run Camel OpenRewrite migration recipes for
upgrading between versions. The project
- must compile successfully before running the recipes, as OpenRewrite
requires a compilable project to parse
- and transform the code.
+| Returns Maven commands to run Camel OpenRewrite migration recipes for
upgrading between versions: the
+ `CamelMigrationRecipe` of `camel-upgrade-recipes` for Camel Main, the
`CamelSpringBootMigrationRecipe` of
+ `camel-spring-boot-upgrade-recipes` for Spring Boot, and the `update` goal
of the `quarkus-maven-plugin` for
+ Quarkus, at the Quarkus platform version given as `quarkusPlatformVersion`
(the one `camel_version_list` reports
+ for the target Camel version), else at the latest recommended platform. The
project must compile successfully
+ before running the recipes, as OpenRewrite requires a compilable project to
parse and transform the code.
| `camel_migration_guide_search`
| Searches Camel migration and upgrade guides for a specific term or component
name. Each result is one topic
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookup.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookup.java
index 68d5e3c2729a..d3f7700ce6c3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookup.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookup.java
@@ -168,17 +168,60 @@ public final class DependencyLookup {
"org.apache.camel:camel-bom:" + camelVersion));
}
if (rt == null || rt.equals("spring-boot")) {
- declare.put("pomSpringBoot", pom("org.apache.camel.springboot",
"camel-" + shortName + "-starter", null,
+ RuntimeArtifact sb = runtimeArtifact("camel-" + shortName,
"spring-boot");
+ declare.put("pomSpringBoot", pom(sb.groupId(), sb.artifactId(),
null,
"org.apache.camel.springboot:camel-spring-boot-bom:" +
camelVersion));
}
if (rt == null || rt.equals("quarkus")) {
- declare.put("pomQuarkus", pom("org.apache.camel.quarkus",
"camel-quarkus-" + shortName, null,
+ RuntimeArtifact q = runtimeArtifact("camel-" + shortName,
"quarkus");
+ declare.put("pomQuarkus", pom(q.groupId(), q.artifactId(), null,
"org.apache.camel.quarkus:camel-quarkus-bom:<camel-quarkus
version>"));
}
answer.put("declare", declare);
return answer;
}
+ /** The Maven coordinates (without version) of a Camel artifact in one
runtime. */
+ public record RuntimeArtifact(String groupId, String artifactId) {
+ }
+
+ /**
+ * The artifact that provides a Camel artifact ({@code camel-kafka}) in a
runtime: {@code camel-kafka} on Camel
+ * Main, {@code org.apache.camel.springboot:camel-kafka-starter} on Spring
Boot and
+ * {@code org.apache.camel.quarkus:camel-quarkus-kafka} on Quarkus.
+ *
+ * @param camelArtifactId the artifact id of the Camel catalog, starting
with {@code camel-}
+ * @param runtime main, spring-boot or quarkus; null means main
+ */
+ public static RuntimeArtifact runtimeArtifact(String camelArtifactId,
String runtime) {
+ String shortName = camelArtifactId.startsWith("camel-") ?
camelArtifactId.substring(6) : camelArtifactId;
+ if ("spring-boot".equals(runtime)) {
+ return new RuntimeArtifact("org.apache.camel.springboot", "camel-"
+ shortName + "-starter");
+ }
+ if ("quarkus".equals(runtime)) {
+ return new RuntimeArtifact("org.apache.camel.quarkus",
"camel-quarkus-" + shortName);
+ }
+ return new RuntimeArtifact("org.apache.camel", "camel-" + shortName);
+ }
+
+ /**
+ * The reverse of {@link #runtimeArtifact}: the Camel catalog artifact id
provided by an artifact of any runtime
+ * ({@code camel-kafka-starter} and {@code camel-quarkus-kafka} both give
{@code camel-kafka}), or the artifact id
+ * itself when it is not a runtime-specific Camel artifact.
+ */
+ public static String camelArtifactId(String artifactId) {
+ if (artifactId == null) {
+ return null;
+ }
+ if (artifactId.startsWith("camel-quarkus-") && artifactId.length() >
14) {
+ return "camel-" + artifactId.substring(14);
+ }
+ if (artifactId.startsWith("camel-") && artifactId.endsWith("-starter")
&& artifactId.length() > 14) {
+ return artifactId.substring(0, artifactId.length() - 8);
+ }
+ return artifactId;
+ }
+
/** A third-party library: the same coordinates in every runtime. */
static JsonObject declare(JsonObject answer, String groupId, String
artifactId, String version, String rt) {
answer.put("groupId", groupId);
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CamelReleases.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CamelReleases.java
new file mode 100644
index 000000000000..2f217ee715a4
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CamelReleases.java
@@ -0,0 +1,126 @@
+/*
+ * 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.commands.mcp;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.tooling.model.ReleaseModel;
+
+/**
+ * Reads the Camel releases known to the catalog ({@link
CamelCatalog#camelReleases()}, the data behind
+ * {@code camel_version_list}): the latest release and the JDK versions a
release supports.
+ */
+final class CamelReleases {
+
+ private static final Pattern RELEASE_VERSION =
Pattern.compile("\\d+\\.\\d+\\.\\d+");
+
+ private CamelReleases() {
+ }
+
+ /**
+ * The newest Camel release in the catalog, or null when the catalog has
no release data. A SNAPSHOT catalog version
+ * is not a release, so this is what a project should be compared with.
+ */
+ static String latest(CamelCatalog catalog) {
+ String latest = null;
+ for (ReleaseModel release : releases(catalog)) {
+ if (latest == null ||
DependencyCheckTools.compareVersions(release.getVersion(), latest) > 0) {
+ latest = release.getVersion();
+ }
+ }
+ return latest;
+ }
+
+ /**
+ * The release that tells the requirements of a version: the release
itself, else the newest release of the same
+ * major.minor (a patch release does not change the JDK baseline). Returns
null for any other version, including a
+ * version newer than every release in the catalog (such as the one in
development): the catalog only learns a
+ * release once it is out, and a new minor version may drop a JDK version
(Camel 4.24 drops Java 17).
+ */
+ static ReleaseModel forVersion(CamelCatalog catalog, String version) {
+ if (version == null || version.isBlank()) {
+ return null;
+ }
+ String plain = version.trim().replace("-SNAPSHOT", "");
+ String[] parts = plain.split("\\.");
+ String minor = parts.length >= 2 ? parts[0] + "." + parts[1] + "." :
null;
+ ReleaseModel exact = null;
+ ReleaseModel sameMinor = null;
+ for (ReleaseModel release : releases(catalog)) {
+ String v = release.getVersion();
+ if (v.equals(plain)) {
+ exact = release;
+ }
+ if (minor != null && v.startsWith(minor)
+ && (sameMinor == null ||
DependencyCheckTools.compareVersions(v, sameMinor.getVersion()) > 0)) {
+ sameMinor = release;
+ }
+ }
+ if (exact != null) {
+ return exact;
+ }
+ return sameMinor;
+ }
+
+ /** The JDK versions a release supports, oldest first (the catalog writes
the old ones as 1.x). */
+ static List<Integer> jdks(ReleaseModel release) {
+ List<Integer> answer = new ArrayList<>();
+ if (release == null || release.getJdk() == null) {
+ return answer;
+ }
+ for (String jdk : release.getJdk().split(",")) {
+ int v = javaVersion(jdk);
+ if (v > 0) {
+ answer.add(v);
+ }
+ }
+ answer.sort(Integer::compare);
+ return answer;
+ }
+
+ /** A Java version as its feature number: 1.8 and 8 are 8, 21.0.2 is 21; 0
when it cannot be read. */
+ static int javaVersion(String version) {
+ if (version == null || version.isBlank()) {
+ return 0;
+ }
+ try {
+ String v = version.trim();
+ if (v.startsWith("1.")) {
+ v = v.substring(2);
+ }
+ return Integer.parseInt(v.split("\\.")[0]);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static List<ReleaseModel> releases(CamelCatalog catalog) {
+ List<ReleaseModel> answer = new ArrayList<>();
+ List<ReleaseModel> all = catalog.camelReleases();
+ if (all != null) {
+ for (ReleaseModel release : all) {
+ if (release.getVersion() != null &&
RELEASE_VERSION.matcher(release.getVersion()).matches()) {
+ answer.add(release);
+ }
+ }
+ }
+ return answer;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
index 0e9a419773ea..01e07e330fac 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
@@ -17,7 +17,10 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -26,6 +29,7 @@ import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.DependencyLookup;
import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
import org.apache.camel.tooling.model.ComponentModel;
@@ -50,8 +54,9 @@ public class DependencyCheckTools {
*/
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Check Camel project dependency hygiene. Given a
pom.xml (and optionally route definitions), "
- + "detects outdated Camel dependencies compared to the
latest catalog version, "
- + "missing Maven dependencies for components used in
routes, "
+ + "detects an outdated Camel version compared to the
latest Camel release, "
+ + "missing Maven dependencies for components used in
routes (Spring Boot starters and "
+ + "Camel Quarkus extensions count as the component
they provide), "
+ "and version conflicts between the Camel BOM and
explicit dependency overrides. "
+ "Returns actionable recommendations with corrected
dependency snippets. "
+ "POM content is automatically sanitized to mask
sensitive data (passwords, tokens, API keys) "
@@ -88,12 +93,12 @@ public class DependencyCheckTools {
pom.camelVersion(), pom.runtimeType(), pom.javaVersion(),
pom.dependencies().size());
// 1. Check for outdated Camel version
- VersionStatus versionStatus = checkVersionStatus(pom, catalog);
+ VersionStatus versionStatus = checkVersionStatus(pom, catalog,
catalogService.getDefaultCatalog());
// 2. Check for missing dependencies from routes
List<MissingDependency> missingDeps = List.of();
if (routes != null && !routes.isBlank()) {
- missingDeps = checkMissingDependencies(routes,
pom.dependencies(), catalog);
+ missingDeps = checkMissingDependencies(routes, pom, catalog);
}
// 3. Check for version conflicts (explicit overrides when BOM is
present)
@@ -120,36 +125,38 @@ public class DependencyCheckTools {
}
/**
- * Check if the project's Camel version is outdated compared to the
catalog version.
+ * Check if the project's Camel version is outdated compared to the latest
Camel release known to the catalog (not
+ * the catalog version, which is a SNAPSHOT on a development build).
*/
- private VersionStatus checkVersionStatus(MigrationData.PomAnalysis pom,
CamelCatalog catalog) {
+ private VersionStatus checkVersionStatus(MigrationData.PomAnalysis pom,
CamelCatalog catalog, CamelCatalog releases) {
String catalogVersion = catalog.getCatalogVersion();
+ String latest = CamelReleases.latest(releases);
+ if (latest == null) {
+ latest = catalogVersion;
+ }
if (pom.camelVersion() == null) {
return new VersionStatus(
- catalogVersion, null, null, "unknown",
+ catalogVersion, latest, null, null, "unknown",
"Could not detect Camel version from pom.xml. "
- + "Check if the
version is defined in a parent POM or BOM.");
+ + "Check if
the version is defined in a parent POM or BOM.");
}
- int comparison = compareVersions(pom.camelVersion(), catalogVersion);
+ int comparison = compareVersions(pom.camelVersion(), latest);
if (comparison < 0) {
return new VersionStatus(
- catalogVersion, pom.camelVersion(), true, "outdated",
- "Project uses Camel " + pom.camelVersion()
- + "
but " + catalogVersion
- + "
is available. "
- +
"Consider upgrading for bug fixes and new features.");
+ catalogVersion, latest, pom.camelVersion(), true,
"outdated",
+ "Project uses Camel " + pom.camelVersion() + " but " +
latest + " is the latest release. "
+
+ "Consider upgrading for bug fixes and new features.");
} else if (comparison == 0) {
return new VersionStatus(
- catalogVersion, pom.camelVersion(), false, "current",
- "Project uses the latest Camel version.");
+ catalogVersion, latest, pom.camelVersion(), false,
"current",
+ "Project uses the latest Camel release.");
} else {
return new VersionStatus(
- catalogVersion, pom.camelVersion(), false, "newer",
- "Project uses Camel " + pom.camelVersion()
- + "
which is newer than catalog version "
- +
catalogVersion + ".");
+ catalogVersion, latest, pom.camelVersion(), false, "newer",
+ "Project uses Camel " + pom.camelVersion() + " which is
newer than the latest release "
+
+ latest + ".");
}
}
@@ -157,8 +164,16 @@ public class DependencyCheckTools {
* Check for components used in routes that are missing from the project's
dependencies.
*/
private List<MissingDependency> checkMissingDependencies(
- String routes, List<String> existingDeps, CamelCatalog catalog) {
+ String routes, MigrationData.PomAnalysis pom, CamelCatalog
catalog) {
List<MissingDependency> missing = new ArrayList<>();
+ String runtime = pom.runtimeType();
+
+ // a Spring Boot starter or Camel Quarkus extension provides the
component of the same name
+ Set<String> declared = new HashSet<>();
+ for (String dep : pom.dependencies()) {
+ declared.add(dep);
+ declared.add(DependencyLookup.camelArtifactId(dep));
+ }
// the components of the routes' endpoints, not every name in the text
(CAMEL-25586)
for (String comp : RouteComponents.of(routes, null, catalog)) {
@@ -176,10 +191,13 @@ public class DependencyCheckTools {
continue;
}
- if (!existingDeps.contains(artifactId)) {
+ if (!declared.contains(artifactId)) {
+ DependencyLookup.RuntimeArtifact ga =
"spring-boot".equals(runtime) || "quarkus".equals(runtime)
+ ? DependencyLookup.runtimeArtifact(artifactId, runtime)
+ : new
DependencyLookup.RuntimeArtifact(model.getGroupId(), artifactId);
missing.add(new MissingDependency(
- comp, artifactId, model.getGroupId(), model.getTitle(),
- formatDependencySnippet(model.getGroupId(),
artifactId)));
+ comp, ga.artifactId(), ga.groupId(), model.getTitle(),
+ formatDependencySnippet(ga.groupId(),
ga.artifactId())));
}
}
@@ -195,6 +213,7 @@ public class DependencyCheckTools {
boolean hasBom = pomContent.contains("camel-bom")
|| pomContent.contains("camel-spring-boot-bom")
|| pomContent.contains("camel-quarkus-bom")
+ || pomContent.contains("quarkus-camel-bom")
|| pomContent.contains("camel-dependencies");
if (!hasBom) {
@@ -233,9 +252,8 @@ public class DependencyCheckTools {
if (versionStatus.outdated() != null && versionStatus.outdated()) {
recommendations.add(new Recommendation(
"medium", "Version Upgrade",
- "Upgrade Camel from " + pom.camelVersion() + " to " +
versionStatus.catalogVersion(),
- "Use camel_migration_compatibility and
camel_migration_recipes tools "
-
+ "to check compatibility and get automated upgrade
commands.",
+ "Upgrade Camel from " + pom.camelVersion() + " to " +
versionStatus.latestVersion(),
+ "Use the camel_migration_recipes tool to get automated
upgrade commands.",
null));
}
@@ -336,8 +354,9 @@ public class DependencyCheckTools {
}
private boolean hasCamelBom(MigrationData.PomAnalysis pom) {
- return pom.dependencies().stream()
- .anyMatch(d -> d.endsWith("-bom") ||
d.equals("camel-dependencies"));
+ return Stream.concat(pom.dependencies().stream(),
pom.managedDependencies().stream())
+ .anyMatch(d -> d.startsWith("camel-") && d.endsWith("-bom") ||
d.equals("camel-dependencies")
+ || d.equals("quarkus-camel-bom"));
}
private String formatDependencySnippet(String groupId, String artifactId) {
@@ -348,7 +367,8 @@ public class DependencyCheckTools {
}
private String formatBomSnippet(MigrationData.PomAnalysis pom) {
- return dependencyData.formatBomSnippet(pom.runtimeType(),
pom.camelVersion());
+ return dependencyData.formatBomSnippet(
+ pom.runtimeType(), pom.camelVersion(), pom.quarkusVersion(),
pom.camelQuarkusVersion());
}
// Result records
@@ -362,8 +382,12 @@ public class DependencyCheckTools {
public record ProjectInfo(String camelVersion, String runtimeType, String
javaVersion, int dependencyCount) {
}
+ /**
+ * @param catalogVersion the version of the catalog this server answers
from
+ * @param latestVersion the latest Camel release, what the project
version is compared with
+ */
public record VersionStatus(
- String catalogVersion, String projectVersion, Boolean outdated,
+ String catalogVersion, String latestVersion, String
projectVersion, Boolean outdated,
String status, String message) {
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyData.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyData.java
index 92023349388b..350e855a0f17 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyData.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyData.java
@@ -92,19 +92,60 @@ public class DependencyData {
}
/**
- * Format a BOM dependency management snippet for a given runtime and
version.
+ * Format a BOM dependency management snippet for a given runtime and
Camel version.
*/
- public String formatBomSnippet(String runtime, String version) {
+ public String formatBomSnippet(String runtime, String camelVersion) {
+ return formatBomSnippet(runtime, camelVersion, null);
+ }
+
+ /**
+ * Format a BOM dependency management snippet for a given runtime, without
a Camel Quarkus version.
+ */
+ public String formatBomSnippet(String runtime, String camelVersion, String
quarkusPlatformVersion) {
+ return formatBomSnippet(runtime, camelVersion, quarkusPlatformVersion,
null);
+ }
+
+ /**
+ * Format a BOM dependency management snippet for a given runtime.
+ * <p>
+ * The Camel version is the version of the Camel and Camel Spring Boot
BOMs only: Camel Quarkus has its own version
+ * line, so a Quarkus project imports {@code
io.quarkus.platform:quarkus-camel-bom} at the Quarkus platform version
+ * when it is known, else the camel-quarkus-bom at the Camel Quarkus
version when it is known, else the
+ * camel-quarkus-bom with a version to look up.
+ *
+ * @param camelVersion the Camel version, or null
+ * @param quarkusPlatformVersion the Quarkus platform version, or null
+ * @param camelQuarkusVersion the Camel Quarkus version, or null
+ */
+ public String formatBomSnippet(
+ String runtime, String camelVersion, String
quarkusPlatformVersion, String camelQuarkusVersion) {
BomTemplate template = getBomTemplate(runtime);
if (template == null) {
template = BOM_TEMPLATES.get(0); // default to main
}
- String ver = version != null ? version : "${camel.version}";
+ String groupId = template.groupId();
+ String artifactId = template.artifactId();
+ String ver = camelVersion != null ? camelVersion : "${camel.version}";
+ String comment = "";
+ if ("quarkus".equals(template.runtime())) {
+ if (quarkusPlatformVersion != null) {
+ groupId = "io.quarkus.platform";
+ artifactId = "quarkus-camel-bom";
+ ver = quarkusPlatformVersion;
+ } else if (camelQuarkusVersion != null) {
+ ver = camelQuarkusVersion;
+ } else {
+ ver = "${camel-quarkus.version}";
+ comment = " <!-- a Camel Quarkus version, not a
Camel version: look it up with "
+ + "camel_version_list runtime=quarkus -->\n";
+ }
+ }
return "<dependencyManagement>\n"
+ " <dependencies>\n"
+ " <dependency>\n"
- + " <groupId>" + template.groupId() + "</groupId>\n"
- + " <artifactId>" + template.artifactId() +
"</artifactId>\n"
+ + comment
+ + " <groupId>" + groupId + "</groupId>\n"
+ + " <artifactId>" + artifactId + "</artifactId>\n"
+ " <version>" + ver + "</version>\n"
+ " <type>pom</type>\n"
+ " <scope>import</scope>\n"
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
index 43ca52fcd94a..1353b38ce5db 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
@@ -17,9 +17,11 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -28,6 +30,7 @@ import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.DependencyLookup;
import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
import org.apache.camel.tooling.model.ComponentModel;
import org.apache.camel.tooling.model.SecurityAdvisoryModel;
@@ -51,10 +54,12 @@ public class DependencySecurityAuditTools {
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Perform a security vulnerability audit on a Camel
project's dependencies. "
- + "Cross-references the project's pom.xml dependencies
with the Camel security advisory "
+ + "Cross-references the project's pom.xml dependencies
(Spring Boot starters and Camel Quarkus "
+ + "extensions count as the component they provide)
with the Camel security advisory "
+ "database to identify CVEs affecting each artifact
at the project's Camel version. "
- + "Reports severity, affected version ranges, fixed
versions, and whether the vulnerable "
- + "component is directly used (reachable) or only a
transitive dependency. "
+ + "Reports severity, affected version ranges, fixed
versions, and, when routes are given, "
+ + "whether the vulnerable component is used by them
(reachable). Advisories that name no "
+ + "component cannot be matched to dependencies and are
listed as uncheckedAdvisories. "
+ "POM content is automatically sanitized to mask
sensitive data.")
public AuditResult camel_dependency_security_audit(
@ToolArg(description = "The pom.xml file content") String
pomContent,
@@ -62,7 +67,9 @@ public class DependencySecurityAuditTools {
+ "are actually used (for reachability
analysis)",
required = false) String routes,
@ToolArg(description = ToolArgDocs.RUNTIME, required = false)
String runtime,
- @ToolArg(description = ToolArgDocs.CAMEL_VERSION, required =
false) String camelVersion,
+ @ToolArg(description = ToolArgDocs.CAMEL_VERSION
+ + " Also the version audited when the
pom.xml does not set it (parent-managed).",
+ required = false) String camelVersion,
@ToolArg(description = ToolArgDocs.PLATFORM_BOM, required = false)
String platformBom,
@ToolArg(description = "If true (default), mask credentials in POM
content",
required = false) Boolean sanitizePom) {
@@ -76,57 +83,69 @@ public class DependencySecurityAuditTools {
CamelCatalog catalog = catalogService.loadCatalog(runtime,
camelVersion, platformBom);
MigrationData.PomAnalysis pom =
MigrationData.parsePomContent(processed.content());
- String effectiveVersion = pom.camelVersion() != null ?
pom.camelVersion() : catalog.getCatalogVersion();
+ List<String> warnings = new ArrayList<>();
+ boolean versionDetected = pom.camelVersion() != null;
+ String effectiveVersion = pom.camelVersion();
+ if (!versionDetected) {
+ if (camelVersion != null && !camelVersion.isBlank() &&
Character.isDigit(camelVersion.trim().charAt(0))) {
+ effectiveVersion = camelVersion.trim();
+ warnings.add("Could not detect the Camel version from
pom.xml (it is likely managed by a parent "
+ + "POM or a platform BOM): audited Camel " +
effectiveVersion
+ + " from the camelVersion argument.");
+ } else {
+ warnings.add("Could not detect the Camel version from
pom.xml (it is likely managed by a parent "
+ + "POM or a platform BOM) and no camelVersion
was given: every advisory naming a "
+ + "declared component is listed, whether or
not it affects the project's version. "
+ + "Call again with camelVersion to audit that
version.");
+ }
+ }
List<SecurityAdvisoryModel> allAdvisories =
advisoryService.advisories();
- List<String> usedSchemes = routes != null && !routes.isBlank()
- ? extractUsedSchemes(routes, catalog) : List.of();
+ boolean routesGiven = routes != null && !routes.isBlank();
+ List<String> usedSchemes = routesGiven ?
extractUsedSchemes(routes, catalog) : List.of();
Map<String, ArtifactAudit> auditByArtifact = new LinkedHashMap<>();
+ Set<String> audited = new HashSet<>();
for (String dep : pom.dependencies()) {
+ // camel-kafka-starter and camel-quarkus-kafka are camel-kafka
for the advisories
+ String camelArtifact = DependencyLookup.camelArtifactId(dep);
+ audited.add(camelArtifact);
List<AdvisoryService.AdvisoryView> matched
- = AdvisoryService.query(allAdvisories,
effectiveVersion, dep, null);
+ = AdvisoryService.query(allAdvisories,
effectiveVersion, camelArtifact, null);
if (!matched.isEmpty()) {
- boolean reachable = isReachable(dep, usedSchemes, catalog);
- List<VulnerabilityFinding> findings = new ArrayList<>();
- for (AdvisoryService.AdvisoryView adv : matched) {
- findings.add(new VulnerabilityFinding(
- adv.cve(),
- adv.summary(),
- adv.severity(),
- adv.affected(),
- adv.fixed(),
- adv.url()));
- }
- auditByArtifact.put(dep, new ArtifactAudit(dep, reachable,
findings));
+ Boolean reachable = routesGiven ?
isReachable(camelArtifact, usedSchemes, catalog) : null;
+ auditByArtifact.put(dep, new ArtifactAudit(dep, reachable,
findings(matched)));
}
}
// Check components used in routes but not declared as POM
dependencies (transitively available)
- for (String compName : catalog.findComponentNames()) {
+ for (String compName : usedSchemes) {
ComponentModel model = catalog.componentModel(compName);
if (model == null || model.getArtifactId() == null) {
continue;
}
String artifactId = model.getArtifactId();
- if (auditByArtifact.containsKey(artifactId)) {
+ if (!audited.add(artifactId)) {
continue;
}
-
List<AdvisoryService.AdvisoryView> matched
= AdvisoryService.query(allAdvisories,
effectiveVersion, artifactId, null);
- if (!matched.isEmpty() && usedSchemes.contains(compName)) {
- List<VulnerabilityFinding> findings = new ArrayList<>();
- for (AdvisoryService.AdvisoryView adv : matched) {
- findings.add(new VulnerabilityFinding(
- adv.cve(), adv.summary(), adv.severity(),
- adv.affected(), adv.fixed(),
- adv.url()));
+ if (!matched.isEmpty()) {
+ auditByArtifact.put(artifactId, new
ArtifactAudit(artifactId, true, findings(matched)));
+ }
+ }
+
+ // advisories that name no component can never match a dependency:
list those that may affect the version
+ List<VulnerabilityFinding> unchecked = new ArrayList<>();
+ for (SecurityAdvisoryModel advisory : allAdvisories) {
+ if (advisory.getComponents() == null ||
advisory.getComponents().isEmpty()) {
+ AdvisoryService.AdvisoryView view =
AdvisoryService.AdvisoryView.of(advisory, effectiveVersion);
+ if (!Boolean.FALSE.equals(view.affectsGivenVersion())) {
+ unchecked.add(finding(view));
}
- auditByArtifact.put(artifactId, new
ArtifactAudit(artifactId, true, findings));
}
}
@@ -136,22 +155,27 @@ public class DependencySecurityAuditTools {
.flatMap(a -> a.findings().stream())
.filter(f -> "critical".equalsIgnoreCase(f.severity()) ||
"high".equalsIgnoreCase(f.severity()))
.count();
- long reachableCount =
vulnerableArtifacts.stream().filter(ArtifactAudit::reachable).count();
+ long reachableCount = vulnerableArtifacts.stream().filter(a ->
Boolean.TRUE.equals(a.reachable())).count();
- List<String> recommendations =
buildRecommendations(vulnerableArtifacts, effectiveVersion, catalog);
+ List<String> recommendations
+ = buildRecommendations(vulnerableArtifacts, unchecked,
effectiveVersion, routesGiven);
AuditSummary summary = new AuditSummary(
effectiveVersion,
+ versionDetected,
pom.dependencies().size(),
vulnerableArtifacts.size(),
totalCves,
(int) criticalCount,
(int) reachableCount,
- totalCves == 0);
+ unchecked.size(),
+ totalCves == 0 && unchecked.isEmpty() && effectiveVersion
!= null);
return new AuditResult(
processed.warnings().isEmpty() ? null :
processed.warnings(),
+ warnings.isEmpty() ? null : warnings,
vulnerableArtifacts.isEmpty() ? null : vulnerableArtifacts,
+ unchecked.isEmpty() ? null : unchecked,
recommendations.isEmpty() ? null : recommendations,
summary);
@@ -163,6 +187,18 @@ public class DependencySecurityAuditTools {
}
}
+ private static List<VulnerabilityFinding>
findings(List<AdvisoryService.AdvisoryView> matched) {
+ List<VulnerabilityFinding> findings = new ArrayList<>();
+ for (AdvisoryService.AdvisoryView adv : matched) {
+ findings.add(finding(adv));
+ }
+ return findings;
+ }
+
+ private static VulnerabilityFinding finding(AdvisoryService.AdvisoryView
adv) {
+ return new VulnerabilityFinding(adv.cve(), adv.summary(),
adv.severity(), adv.affected(), adv.fixed(), adv.url());
+ }
+
/**
* The components the routes use: the components of their endpoints, read
as routes of their DSL (CAMEL-25586).
* Tokenizing the route text instead would treat the DSL's own structural
keywords ({@code from:}, {@code to:},
@@ -172,10 +208,8 @@ public class DependencySecurityAuditTools {
return RouteComponents.of(routes, null, catalog);
}
+ /** Whether the routes use the component of the (catalog) artifact; only
called when routes were given. */
private boolean isReachable(String artifactId, List<String> usedSchemes,
CamelCatalog catalog) {
- if (usedSchemes.isEmpty()) {
- return true;
- }
for (String scheme : usedSchemes) {
ComponentModel model = catalog.componentModel(scheme);
if (model != null && artifactId.equals(model.getArtifactId())) {
@@ -187,7 +221,8 @@ public class DependencySecurityAuditTools {
}
private List<String> buildRecommendations(
- List<ArtifactAudit> vulnerableArtifacts, String version,
CamelCatalog catalog) {
+ List<ArtifactAudit> vulnerableArtifacts,
List<VulnerabilityFinding> unchecked, String version,
+ boolean routesGiven) {
List<String> recs = new ArrayList<>();
boolean hasCritical = vulnerableArtifacts.stream()
@@ -195,30 +230,38 @@ public class DependencySecurityAuditTools {
.anyMatch(f -> "critical".equalsIgnoreCase(f.severity()));
if (hasCritical) {
recs.add("URGENT: Critical vulnerabilities found. Upgrade Camel
version immediately. "
- + "Use camel_migration_compatibility to check upgrade
path.");
- }
-
- long reachableVulnCount =
vulnerableArtifacts.stream().filter(ArtifactAudit::reachable).count();
- if (reachableVulnCount > 0) {
- recs.add("Found " + reachableVulnCount
- + " vulnerable artifact(s) that are directly used in
routes. "
- + "These are reachable and should be prioritized for
patching.");
+ + "Use camel_migration_recipes to get the upgrade
commands.");
}
- long unreachableCount = vulnerableArtifacts.stream().filter(a ->
!a.reachable()).count();
- if (unreachableCount > 0) {
- recs.add("Found " + unreachableCount
- + " vulnerable artifact(s) not directly used in routes. "
- + "These are lower priority but should still be
evaluated.");
+ if (routesGiven) {
+ long reachableVulnCount = vulnerableArtifacts.stream().filter(a ->
Boolean.TRUE.equals(a.reachable())).count();
+ if (reachableVulnCount > 0) {
+ recs.add("Found " + reachableVulnCount
+ + " vulnerable artifact(s) that are directly used in
routes. "
+ + "These are reachable and should be prioritized for
patching.");
+ }
+ long unreachableCount = vulnerableArtifacts.stream().filter(a ->
Boolean.FALSE.equals(a.reachable())).count();
+ if (unreachableCount > 0) {
+ recs.add("Found " + unreachableCount
+ + " vulnerable artifact(s) not directly used in
routes. "
+ + "These are lower priority but should still be
evaluated.");
+ }
+ } else if (!vulnerableArtifacts.isEmpty()) {
+ recs.add("Found " + vulnerableArtifacts.size() + " vulnerable
artifact(s). Pass the routes to tell which "
+ + "of them the routes use (reachable).");
}
- String latestVersion = catalog.getCatalogVersion();
- if (DependencyCheckTools.compareVersions(version, latestVersion) < 0) {
+ String latestVersion =
CamelReleases.latest(catalogService.getDefaultCatalog());
+ if (version != null && latestVersion != null &&
DependencyCheckTools.compareVersions(version, latestVersion) < 0) {
recs.add("Upgrade from Camel " + version + " to " + latestVersion
+ " to pick up security fixes. Use
camel_migration_recipes for automated upgrade.");
}
- if (vulnerableArtifacts.isEmpty()) {
+ if (!unchecked.isEmpty()) {
+ recs.add(unchecked.size() + " advisory(ies) name no component, so
they could not be matched to the "
+ + "dependencies; review uncheckedAdvisories against the
project.");
+ }
+ if (vulnerableArtifacts.isEmpty() && unchecked.isEmpty() && version !=
null) {
recs.add("No known Camel CVEs affect the declared dependencies at
version " + version + ".");
}
@@ -229,14 +272,19 @@ public class DependencySecurityAuditTools {
public record AuditResult(
List<String> sanitizationWarnings,
+ List<String> warnings,
List<ArtifactAudit> vulnerableArtifacts,
+ List<VulnerabilityFinding> uncheckedAdvisories,
List<String> recommendations,
AuditSummary summary) {
}
+ /**
+ * @param reachable whether the routes use the artifact's component; null
when no routes were given
+ */
public record ArtifactAudit(
String artifactId,
- boolean reachable,
+ Boolean reachable,
List<VulnerabilityFinding> findings) {
}
@@ -249,13 +297,21 @@ public class DependencySecurityAuditTools {
String advisoryUrl) {
}
+ /**
+ * @param camelVersion the audited Camel version: the project's,
else the camelVersion argument, else null
+ * @param camelVersionDetected whether the version was read from the
pom.xml
+ * @param uncheckedAdvisories advisories that may affect the version but
name no component to match
+ * @param clean no matched CVE, no unchecked advisory, and
a known version
+ */
public record AuditSummary(
String camelVersion,
+ boolean camelVersionDetected,
int totalDependencies,
int vulnerableArtifacts,
int totalCves,
int criticalAndHighCves,
int reachableVulnerableArtifacts,
+ int uncheckedAdvisories,
boolean clean) {
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationData.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationData.java
index e764c8a19d1e..4781c6f96013 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationData.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationData.java
@@ -44,6 +44,7 @@ import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.apache.commons.text.similarity.LevenshteinDistance;
@@ -94,7 +95,7 @@ public class MigrationData {
"org.wildfly.camel", "org.wildfly",
"org.wildfly.plugins",
"javax.ejb", "jakarta.ejb",
- "camel-cdi", "maven-war-plugin");
+ "camel-cdi");
private static final List<String> KARAF_MARKERS = Arrays.asList(
"org.apache.karaf", "camel-blueprint",
@@ -144,25 +145,31 @@ public class MigrationData {
/**
* Parse pom.xml content and extract project analysis data.
+ * <p>
+ * The declared Camel dependencies are the {@code <dependency>} elements
of the project (and its profiles), not the
+ * ones in {@code <dependencyManagement>} (a BOM or a managed version
declares nothing) nor the dependencies of a
+ * plugin. Runtime markers are read from dependencies, plugins and the
parent alike.
*/
public static PomAnalysis parsePomContent(String pomContent) throws
Exception {
DocumentBuilderFactory factory = createSecureDocumentBuilderFactory();
Document doc = factory.newDocumentBuilder()
.parse(new
ByteArrayInputStream(pomContent.getBytes(StandardCharsets.UTF_8)));
+ Element project = doc.getDocumentElement();
String camelVersion = null;
+ String camelQuarkusVersion = null;
String springBootVersion = null;
String quarkusVersion = null;
String javaVersion = null;
- boolean isWildfly = false;
- boolean isKaraf = false;
List<String> dependencies = new ArrayList<>();
+ List<String> managedDependencies = new ArrayList<>();
- // Detect packaging type (war packaging is a strong WildFly/app-server
signal)
- String packaging = getElementText(doc.getDocumentElement(),
"packaging");
- if ("war".equalsIgnoreCase(packaging)) {
- isWildfly = true;
- }
+ // war packaging alone says only that the project is deployed to a
servlet container or application server:
+ // a Spring Boot WAR is deployed to Tomcat, so it is a WildFly signal
only without Spring Boot or Quarkus
+ String packaging = getChildText(project, "packaging");
+ boolean war = "war".equalsIgnoreCase(packaging);
+ boolean wildfly = false;
+ boolean karaf = "bundle".equalsIgnoreCase(packaging);
// Extract properties
NodeList propertiesNodes = doc.getElementsByTagName("properties");
@@ -172,6 +179,7 @@ public class MigrationData {
if (camelVersion == null) {
camelVersion = getElementText(properties, "camel-version");
}
+ camelQuarkusVersion = getElementText(properties,
"camel-quarkus.version");
springBootVersion = getElementText(properties,
"spring-boot.version");
if (springBootVersion == null) {
springBootVersion = getElementText(properties,
"spring-boot-version");
@@ -188,67 +196,146 @@ public class MigrationData {
javaVersion = getElementText(properties,
"maven.compiler.target");
}
}
+ boolean springBoot = springBootVersion != null;
+ boolean quarkus = quarkusVersion != null;
+
+ Element parent = getChild(project, "parent");
+ if (parent != null) {
+ String artifactId = getChildText(parent, "artifactId");
+ String version = getChildText(parent, "version");
+ if ("spring-boot-starter-parent".equals(artifactId) ||
"spring-boot-dependencies".equals(artifactId)) {
+ springBoot = true;
+ if (springBootVersion == null && version != null &&
!version.startsWith("$")) {
+ springBootVersion = version;
+ }
+ }
+ }
- // Scan dependencyManagement and dependencies for version and runtime
detection
+ // Scan dependencyManagement, dependencies and plugin dependencies for
version and runtime detection
NodeList allDeps = doc.getElementsByTagName("dependency");
for (int i = 0; i < allDeps.getLength(); i++) {
Element dep = (Element) allDeps.item(i);
- String groupId = getElementText(dep, "groupId");
- String artifactId = getElementText(dep, "artifactId");
- String version = getElementText(dep, "version");
+ String groupId = getChildText(dep, "groupId");
+ String artifactId = getChildText(dep, "artifactId");
+ String version = getChildText(dep, "version");
if (groupId == null || artifactId == null) {
continue;
}
+ boolean fixedVersion = version != null && !version.startsWith("$");
- // Detect Camel version from BOM
- if (camelVersion == null && version != null &&
!version.startsWith("$")) {
- if ("camel-bom".equals(artifactId) ||
"camel-spring-boot-bom".equals(artifactId)
- || "camel-quarkus-bom".equals(artifactId)) {
- camelVersion = version;
- }
+ // Detect Camel version from BOM (the camel-quarkus-bom version is
a Camel Quarkus version)
+ if (camelVersion == null && fixedVersion
+ && ("camel-bom".equals(artifactId) ||
"camel-spring-boot-bom".equals(artifactId))) {
+ camelVersion = version;
+ }
+ if (camelQuarkusVersion == null && fixedVersion &&
"camel-quarkus-bom".equals(artifactId)) {
+ camelQuarkusVersion = version;
}
- // Detect Spring Boot
- if (springBootVersion == null && version != null &&
!version.startsWith("$")) {
- if ("spring-boot-dependencies".equals(artifactId)
- || "spring-boot-starter-parent".equals(artifactId)) {
- springBootVersion = version;
- }
+ // Detect Spring Boot by the group: an artifact name with
spring-boot is not enough, such as
+ // io.quarkus:quarkus-spring-boot-properties in a Camel Quarkus
project
+ if ("org.springframework.boot".equals(groupId) ||
"org.apache.camel.springboot".equals(groupId)) {
+ springBoot = true;
+ }
+ if (springBootVersion == null && fixedVersion
+ && ("spring-boot-dependencies".equals(artifactId)
+ ||
"spring-boot-starter-parent".equals(artifactId))) {
+ springBootVersion = version;
}
// Detect Quarkus
- if (quarkusVersion == null && version != null &&
!version.startsWith("$")) {
- if ("quarkus-bom".equals(artifactId)) {
- quarkusVersion = version;
- }
+ if (groupId.startsWith("io.quarkus") ||
artifactId.startsWith("camel-quarkus-")) {
+ quarkus = true;
}
-
- // Detect WildFly
- for (String marker : WILDFLY_MARKERS) {
- if (groupId.contains(marker) || artifactId.contains(marker)) {
- isWildfly = true;
- }
+ if (quarkusVersion == null && fixedVersion
+ && ("quarkus-bom".equals(artifactId) ||
"quarkus-camel-bom".equals(artifactId))) {
+ quarkusVersion = version;
}
- // Detect Karaf
- for (String marker : KARAF_MARKERS) {
- if (groupId.contains(marker) || artifactId.contains(marker)) {
- isKaraf = true;
- }
- }
+ wildfly |= matchesMarker(groupId, artifactId, WILDFLY_MARKERS);
+ karaf |= matchesMarker(groupId, artifactId, KARAF_MARKERS);
- // Collect Camel dependencies
- if (artifactId.startsWith("camel-")) {
+ if (hasAncestor(dep, "dependencyManagement")) {
+ managedDependencies.add(artifactId);
+ } else if (artifactId.startsWith("camel-") && !hasAncestor(dep,
"plugin")) {
dependencies.add(artifactId);
}
}
+ // Plugins carry the WAR, WildFly and OSGi bundle markers
+ NodeList plugins = doc.getElementsByTagName("plugin");
+ for (int i = 0; i < plugins.getLength(); i++) {
+ Element plugin = (Element) plugins.item(i);
+ String groupId = getChildText(plugin, "groupId");
+ String artifactId = getChildText(plugin, "artifactId");
+ if (artifactId == null) {
+ continue;
+ }
+ if (groupId == null) {
+ groupId = "org.apache.maven.plugins";
+ }
+ if ("maven-war-plugin".equals(artifactId)) {
+ war = true;
+ } else if ("spring-boot-maven-plugin".equals(artifactId)) {
+ springBoot = true;
+ } else if ("quarkus-maven-plugin".equals(artifactId)) {
+ quarkus = true;
+ }
+ wildfly |= matchesMarker(groupId, artifactId, WILDFLY_MARKERS);
+ karaf |= matchesMarker(groupId, artifactId, KARAF_MARKERS);
+ }
+
+ if (war && !springBoot && !quarkus) {
+ wildfly = true;
+ }
+
return new PomAnalysis(
- camelVersion, springBootVersion, quarkusVersion, javaVersion,
- dependencies, isWildfly, isKaraf);
+ camelVersion, camelQuarkusVersion, springBootVersion,
quarkusVersion, javaVersion,
+ dependencies, managedDependencies, wildfly, karaf, springBoot,
quarkus);
}
+ private static boolean matchesMarker(String groupId, String artifactId,
List<String> markers) {
+ for (String marker : markers) {
+ if (groupId.contains(marker) || artifactId.contains(marker)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasAncestor(Element element, String tagName) {
+ for (Node node = element.getParentNode(); node != null; node =
node.getParentNode()) {
+ if (node instanceof Element e && tagName.equals(localName(e))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Element getChild(Element parent, String tagName) {
+ for (Node node = parent.getFirstChild(); node != null; node =
node.getNextSibling()) {
+ if (node instanceof Element e && tagName.equals(localName(e))) {
+ return e;
+ }
+ }
+ return null;
+ }
+
+ private static String getChildText(Element parent, String tagName) {
+ Element child = getChild(parent, tagName);
+ if (child != null) {
+ String text = child.getTextContent();
+ if (text != null && !text.isBlank()) {
+ return text.trim();
+ }
+ }
+ return null;
+ }
+
+ private static String localName(Element element) {
+ return element.getLocalName() != null ? element.getLocalName() :
element.getTagName();
+ }
// XML helper
private static DocumentBuilderFactory createSecureDocumentBuilderFactory()
{
@@ -827,14 +914,28 @@ public class MigrationData {
private record GuideIndex(List<GuideSection> sections, Map<String,
Integer> documentFrequencies, double averageLength) {
}
+ /**
+ * What {@link #parsePomContent} reads from a pom.xml.
+ *
+ * @param camelVersion the Camel version (camel.version property,
camel-bom or camel-spring-boot-bom), or
+ * null
+ * @param camelQuarkusVersion the Camel Quarkus version
(camel-quarkus.version property or camel-quarkus-bom), or
+ * null
+ * @param dependencies the Camel artifacts the project declares as
dependencies
+ * @param managedDependencies the artifacts in dependencyManagement (BOMs,
managed versions): declared nowhere
+ */
public record PomAnalysis(
String camelVersion,
+ String camelQuarkusVersion,
String springBootVersion,
String quarkusVersion,
String javaVersion,
List<String> dependencies,
+ List<String> managedDependencies,
boolean isWildfly,
- boolean isKaraf) {
+ boolean isKaraf,
+ boolean isSpringBoot,
+ boolean isQuarkus) {
/**
* Determine the runtime type from the POM analysis.
@@ -846,10 +947,10 @@ public class MigrationData {
if (isKaraf) {
return "karaf";
}
- if (springBootVersion != null || dependencies.stream().anyMatch(d
-> d.contains("spring-boot"))) {
+ if (isSpringBoot) {
return "spring-boot";
}
- if (quarkusVersion != null || dependencies.stream().anyMatch(d ->
d.contains("quarkus"))) {
+ if (isQuarkus) {
return "quarkus";
}
return "main";
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
index e5fe38144c71..7ec2bc9c343b 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
@@ -26,6 +26,7 @@ import jakarta.inject.Inject;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.tooling.model.ReleaseModel;
/**
* MCP Tools for Camel migration and upgrade workflows.
@@ -38,14 +39,20 @@ import io.quarkiverse.mcp.server.ToolCallException;
@ApplicationScoped
public class MigrationTools {
- private static final String OPENREWRITE_VERSION = "6.29.0";
private static final String CAMEL_UPGRADE_RECIPES_ARTIFACT =
"camel-upgrade-recipes";
private static final String CAMEL_SPRING_BOOT_UPGRADE_RECIPES_ARTIFACT =
"camel-spring-boot-upgrade-recipes";
+ // the recipes of META-INF/rewrite/latest.yaml in each artifact of
apache/camel-upgrade-recipes
+ private static final String CAMEL_MIGRATION_RECIPE =
"org.apache.camel.upgrade.CamelMigrationRecipe";
+ private static final String CAMEL_SPRING_BOOT_MIGRATION_RECIPE
+ = "org.apache.camel.upgrade.CamelSpringBootMigrationRecipe";
private static final int SNIPPET_LINES = 30;
@Inject
MigrationData migrationData;
+ @Inject
+ CatalogService catalogService;
+
/**
* Step 1: Analyze a project's pom.xml to detect runtime, Camel version,
Java version, and components.
*/
@@ -91,7 +98,7 @@ public class MigrationTools {
if ("wildfly".equals(runtimeType) || "karaf".equals(runtimeType)) {
nextStep = "Call camel_migration_wildfly_karaf to get
migration guidance for " + runtimeType + ".";
} else {
- nextStep = "Call camel_migration_compatibility with the
detected components and target version.";
+ nextStep = "Call camel_migration_compatibility with the
detected and the target Camel version.";
}
return new ProjectAnalysisResult(
@@ -108,19 +115,16 @@ public class MigrationTools {
* Step 2: Check compatibility and provide relevant migration guide
references.
*/
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
- description = "Check migration compatibility for Camel components by
providing relevant migration guide "
- + "URLs and Java version requirements. The LLM should
consult the migration guides for "
- + "detailed component rename mappings and API
changes.")
+ description = "Check whether a migration between two Camel versions
is possible on the project's Java version "
+ + "(the JDK versions each Camel release supports, as
camel_version_list reports them) and "
+ + "return the relevant migration guide URLs. The LLM
should consult the migration guides, or "
+ + "camel_migration_guide_search, for component renames
and API changes.")
public CompatibilityResult camel_migration_compatibility(
- @ToolArg(description = "Comma-separated list of Camel component
artifactIds") String camelComponents,
@ToolArg(description = "Current Camel version (e.g., 3.20.0)")
String currentVersion,
@ToolArg(description = "Target Camel version (e.g., 4.18.0)")
String targetVersion,
@ToolArg(description = ToolArgDocs.RUNTIME_REQUIRED) String
runtime,
@ToolArg(description = "Current Java version (e.g., 11, 17, 21)",
required = false) String javaVersion) {
- if (camelComponents == null || camelComponents.isBlank()) {
- throw new ToolCallException("camelComponents is required", null);
- }
if (currentVersion == null || currentVersion.isBlank()) {
throw new ToolCallException("currentVersion is required", null);
}
@@ -138,30 +142,19 @@ public class MigrationTools {
JavaCompatibility javaCompat = null;
if (javaVersion != null && !javaVersion.isBlank()) {
- int javaVer = parseJavaVersion(javaVersion);
- boolean javaCompatible = true;
- String requiredVersion;
-
- if (targetMajor >= 4) {
- int targetMinor = parseMinorVersion(targetVersion);
- if (targetMajor > 4 || targetMinor >= 19) {
- requiredVersion = "21";
- if (javaVer < 21) {
- javaCompatible = false;
- blockers.add("Camel 4.19+ requires Java 21+. Current
Java version is " + javaVersion + ".");
- }
- } else {
- requiredVersion = "17";
- if (javaVer < 17) {
- javaCompatible = false;
- blockers.add("Camel 4.x requires Java 17+. Current
Java version is " + javaVersion + ".");
- }
- }
+ JavaRequirement requirement = javaRequirement(targetVersion);
+ if (requirement == null) {
+ warnings.add("The catalog has no release data for Camel " +
targetVersion
+ + ": check its Java requirement with
camel_version_list.");
} else {
- requiredVersion = "11";
+ int javaVer = CamelReleases.javaVersion(javaVersion);
+ boolean javaCompatible = javaVer >= requirement.minimum();
+ if (!javaCompatible) {
+ blockers.add(requirement.describe(targetVersion) + "
Current Java version is " + javaVersion + ".");
+ }
+ javaCompat = new JavaCompatibility(
+ javaVersion, String.valueOf(requirement.minimum()),
requirement.supported(), javaCompatible);
}
-
- javaCompat = new JavaCompatibility(javaVersion, requiredVersion,
javaCompatible);
}
// Collect relevant migration guides
@@ -208,7 +201,9 @@ public class MigrationTools {
*/
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Get Maven commands to run Camel OpenRewrite migration
recipes for upgrading between versions. "
- + "Returns the exact Maven commands to execute on the
project. "
+ + "Returns the exact Maven commands to execute on the
project: camel-upgrade-recipes for "
+ + "Camel Main, camel-spring-boot-upgrade-recipes for
Spring Boot, and the quarkus-maven-plugin "
+ + "update goal for Quarkus. "
+ "PREREQUISITE: The project MUST compile successfully
('mvn clean compile' must pass) "
+ "BEFORE running the OpenRewrite recipes. If the
project does not compile, fix the build "
+ "errors first. OpenRewrite requires a compilable
project to parse and transform the code.")
@@ -218,7 +213,11 @@ public class MigrationTools {
@ToolArg(description = "Target Camel version (e.g., 4.18.0)")
String targetVersion,
@ToolArg(description = "Current Java version (e.g., 11, 17)",
required = false) String javaVersion,
@ToolArg(description = "If true, perform a dry run without making
changes (default: true)",
- required = false) Boolean dryRun) {
+ required = false) Boolean dryRun,
+ @ToolArg(description = "Quarkus only: the Quarkus platform version
to update to (e.g. 3.27.1), the one "
+ + "camel_version_list runtime=quarkus
reports for the target Camel version. "
+ + "Without it the update goes to the latest
recommended Quarkus platform.",
+ required = false) String quarkusPlatformVersion) {
if (runtime == null || runtime.isBlank()) {
throw new ToolCallException("runtime is required", null);
@@ -238,17 +237,25 @@ public class MigrationTools {
// The quarkus-maven-plugin:update command internally uses
OpenRewrite recipes
// including Camel-specific recipes (e.g.,
io.quarkus.updates.camel.camel44.CamelQuarkusMigrationRecipe)
// to handle both Quarkus platform and Camel Quarkus upgrades.
- String quarkusCommand;
- if (targetVersion != null && !targetVersion.isBlank()) {
- quarkusCommand = String.format(
- "mvn --no-transfer-progress
io.quarkus.platform:quarkus-maven-plugin:%s:update %s",
- targetVersion, isDryRun ? "-DrewriteDryRun" :
"-DrewriteFullRun");
+ // The plugin is versioned like the Quarkus platform, not like
Camel; its update goal reads
+ // platformVersion (or stream), rewriteDryRun and rewrite.
+ String rewriteMode = isDryRun ? "-DrewriteDryRun=true" :
"-Drewrite=true";
+ if (quarkusPlatformVersion != null &&
!quarkusPlatformVersion.isBlank()) {
+ String qv = quarkusPlatformVersion.trim();
+ mavenCommands.add(String.format(
+ "mvn --no-transfer-progress
io.quarkus.platform:quarkus-maven-plugin:%s:update "
+ + "-DplatformVersion=%s %s",
+ qv, qv, rewriteMode));
} else {
- quarkusCommand = String.format(
- "mvn --no-transfer-progress
io.quarkus.platform:quarkus-maven-plugin:update %s",
- isDryRun ? "-DrewriteDryRun" : "-DrewriteFullRun");
+ mavenCommands.add(String.format(
+ "mvn -U --no-transfer-progress
io.quarkus.platform:quarkus-maven-plugin:update %s",
+ rewriteMode));
+ notes.add("No quarkusPlatformVersion was given, so the update
goes to the latest recommended Quarkus "
+ + "platform, which may ship a newer Camel than " +
targetVersion
+ + ". To stop at Camel " + targetVersion
+ + ", call camel_version_list with runtime=quarkus,
take the Quarkus platform version of "
+ + "that Camel release and call this tool again with
quarkusPlatformVersion.");
}
- mavenCommands.add(quarkusCommand);
notes.add("The quarkus-maven-plugin:update command uses
OpenRewrite under the hood. "
+ "It automatically applies both Quarkus platform
upgrade recipes and Camel Quarkus "
+ "migration recipes (e.g.,
CamelQuarkusMigrationRecipe). "
@@ -259,26 +266,36 @@ public class MigrationTools {
+ "you must first create a new Quarkus project using the
archetype from "
+ "camel_migration_wildfly_karaf, then run this update
command.");
} else {
- String artifact = "spring-boot".equals(resolvedRuntime)
- ? CAMEL_SPRING_BOOT_UPGRADE_RECIPES_ARTIFACT
- : CAMEL_UPGRADE_RECIPES_ARTIFACT;
+ boolean springBoot = "spring-boot".equals(resolvedRuntime);
+ String artifact = springBoot ?
CAMEL_SPRING_BOOT_UPGRADE_RECIPES_ARTIFACT : CAMEL_UPGRADE_RECIPES_ARTIFACT;
+ String recipe = springBoot ? CAMEL_SPRING_BOOT_MIGRATION_RECIPE :
CAMEL_MIGRATION_RECIPE;
+ String recipesVersion = recipesVersion(targetVersion);
+ // the recipes README runs the plugin without a version (with -U),
so Maven resolves its latest release
String command = String.format(
- "mvn --no-transfer-progress
org.openrewrite.maven:rewrite-maven-plugin:%s:%s "
+ "mvn -U --no-transfer-progress
org.openrewrite.maven:rewrite-maven-plugin:%s "
+
"-Drewrite.recipeArtifactCoordinates=org.apache.camel.upgrade:%s:%s "
- +
"-Drewrite.activeRecipes=org.apache.camel.upgrade.CamelMigrationRecipe",
- OPENREWRITE_VERSION, runMode, artifact, targetVersion);
+ + "-Drewrite.activeRecipes=%s",
+ runMode, artifact, recipesVersion, recipe);
mavenCommands.add(command);
+ notes.add("The recipes are released once per Camel minor version
(" + artifact + ":<major>.<minor>.0), "
+ + "and " + recipe + " of version " + recipesVersion + "
migrates the project to Camel "
+ + recipesVersion + ". For a patch release, set the Camel
version afterwards. If "
+ + recipesVersion + " is not published (there is no
recipes release for every minor version), "
+ + "use the next published one, which migrates to its own
version, or LATEST for the newest.");
}
- // Java upgrade suggestion
+ // Java upgrade suggestion, from the JDK versions of the target release
List<String> javaUpgradeSuggestions = new ArrayList<>();
if (javaVersion != null && !javaVersion.isBlank()) {
- int javaVer = parseJavaVersion(javaVersion);
- if (javaVer < 21) {
- javaUpgradeSuggestions.add(
- "Camel 4.19+ requires Java 21+. "
- + "OpenRewrite recipe:
org.openrewrite.java.migrate.UpgradeToJava21");
+ JavaRequirement requirement = javaRequirement(targetVersion);
+ int javaVer = CamelReleases.javaVersion(javaVersion);
+ if (requirement != null && javaVer > 0 && javaVer <
requirement.minimum()) {
+ String suggestion = requirement.describe(targetVersion) + "
The project uses Java " + javaVersion + ".";
+ if (requirement.minimum() == 17 &&
!"quarkus".equals(resolvedRuntime)) {
+ suggestion += " The Camel recipes include
org.apache.camel.upgrade.UpgradeToJava17.";
+ }
+ javaUpgradeSuggestions.add(suggestion);
}
}
@@ -358,32 +375,47 @@ public class MigrationTools {
}
}
- private int parseMinorVersion(String version) {
- if (version == null) {
- return 0;
- }
- try {
- String[] parts = version.split("\\.");
- if (parts.length >= 2) {
- return Integer.parseInt(parts[1]);
- }
- return 0;
- } catch (NumberFormatException e) {
- return 0;
+ /**
+ * The camel-upgrade-recipes version for a target Camel version: the
recipes are released per minor version.
+ */
+ static String recipesVersion(String targetVersion) {
+ String[] parts = targetVersion.trim().split("[.-]");
+ if (parts.length >= 2 && isNumber(parts[0]) && isNumber(parts[1])) {
+ return parts[0] + "." + parts[1] + ".0";
}
+ return targetVersion.trim();
}
- private int parseJavaVersion(String version) {
- if (version == null) {
- return 0;
+ private static boolean isNumber(String s) {
+ return !s.isEmpty() && s.chars().allMatch(Character::isDigit);
+ }
+
+ /**
+ * The Java versions the target Camel version supports, from the catalog's
release data, or null when the catalog
+ * does not know the version.
+ */
+ private JavaRequirement javaRequirement(String targetVersion) {
+ ReleaseModel release =
CamelReleases.forVersion(catalogService.getDefaultCatalog(), targetVersion);
+ List<Integer> jdks = CamelReleases.jdks(release);
+ if (jdks.isEmpty()) {
+ return null;
}
- try {
- if (version.startsWith("1.")) {
- return Integer.parseInt(version.substring(2));
- }
- return Integer.parseInt(version.split("\\.")[0]);
- } catch (NumberFormatException e) {
- return 0;
+ return new JavaRequirement(
+ release.getVersion(), jdks.get(0),
+
jdks.stream().map(String::valueOf).collect(Collectors.joining(", ")));
+ }
+
+ /**
+ * @param release the release whose data was used (the target itself, or
the release of the same line)
+ * @param minimum the oldest supported Java version
+ * @param supported the supported Java versions, e.g. "17, 21, 25"
+ */
+ private record JavaRequirement(String release, int minimum, String
supported) {
+
+ String describe(String targetVersion) {
+ String source = release.equals(targetVersion.trim()) ? "" : " (as
Camel " + release + ")";
+ return "Camel " + targetVersion.trim() + source + " requires Java
" + minimum + "+ (supported: "
+ + supported + ").";
}
}
@@ -409,9 +441,14 @@ public class MigrationTools {
String nextStep) {
}
+ /**
+ * @param requiredVersion the oldest Java version the target Camel
version supports
+ * @param supportedVersions the Java versions the target Camel version
supports, e.g. "17, 21, 25"
+ */
public record JavaCompatibility(
String currentVersion,
String requiredVersion,
+ String supportedVersions,
boolean compatible) {
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
index bb8b13d151de..92383d9696b3 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
@@ -133,9 +133,11 @@ public class PromptDefinitions {
3. **Compatibility** (based on detected runtime):
- **wildfly** or **karaf**: call
`camel_migration_wildfly_karaf` with the pom, target runtime, and target
version.
- **main**, **spring-boot**, or **quarkus**: call
`camel_migration_compatibility` \
- with components, current/target version, runtime, and
Java version.
- 4. **Recipes**: call `camel_migration_recipes` with
runtime, versions, Java version, and `dryRun=true` \
- to get the OpenRewrite Maven commands.
+ with `currentVersion`, `targetVersion`, `runtime`, and
`javaVersion`.
+ 4. **Recipes**: call `camel_migration_recipes` with
`runtime`, `currentVersion`, `targetVersion`, \
+ `javaVersion`, and `dryRun=true` to get the
OpenRewrite Maven commands. For **quarkus**, also pass \
+ `quarkusPlatformVersion`: the Quarkus version
`camel_version_list` with `runtime=quarkus` reports \
+ for the target version (otherwise the update goes to
the latest Quarkus platform).
5. **Breaking changes**: for each detected component,
call `camel_migration_guide_search` \
with the current and target Camel versions as
`currentVersion` and `targetVersion` \
to find renames and API changes.
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
index 17a4d2fbc768..675f90eb86bd 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
@@ -35,8 +35,9 @@ import io.quarkiverse.mcp.server.ToolCallException;
*
* Most Camel properties are runtime-agnostic ({@code camel.main.*}, {@code
camel.component.*}, etc.) and are returned
* unchanged. The properties that actually differ between runtimes are the
embedded HTTP server and management endpoints
- * ({@code camel.server.*} / {@code camel.management.*} are jbang-only) and
the legacy {@code camel.springboot.*}
- * configuration that was harmonized to {@code camel.main.*} in Camel 4.5 and
removed in 4.13.
+ * ({@code camel.server.*} / {@code camel.management.*} are camel-main's, with
camel-platform-http-main) and the legacy
+ * {@code camel.springboot.*} configuration that was harmonized to {@code
camel.main.*} in Camel 4.5 and removed in
+ * 4.13, except the options that exist on Spring Boot only.
*/
@McpSecured
@ApplicationScoped
@@ -151,6 +152,21 @@ public class PropertiesTranslateTools {
return m;
}
+ /**
+ * The options of camel-spring-boot that camel-main does not have, by
their name after {@code camel.springboot.}
+ * (lower case, without dashes), with their key on Spring Boot today. In
4.13 the deprecated
+ * {@code camel.springboot.*} options were removed and these moved to
{@code camel.main.*}, except
+ * lenient-configuration-binding, which keeps its {@code
camel.springboot.} name; main-run-controller is deprecated
+ * in favour of run-controller since 4.14.
+ */
+ private static final Map<String, String> SPRING_BOOT_ONLY_OPTIONS = Map.of(
+ "lenientconfigurationbinding",
"camel.springboot.lenient-configuration-binding",
+ "mainruncontroller", "camel.main.run-controller",
+ "runcontroller", "camel.main.run-controller",
+ "includenonsingletons", "camel.main.include-non-singletons",
+ "warnonearlyshutdown", "camel.main.warn-on-early-shutdown",
+ "typeconversion", "camel.main.type-conversion");
+
/**
* Camel property prefixes that are runtime-agnostic and must NOT be
rewritten when changing runtime.
*/
@@ -195,13 +211,17 @@ public class PropertiesTranslateTools {
+
"actually differ are the embedded HTTP server and
management endpoint configuration "
+
- "(camel.server.* / camel.management.* are jbang-only;
camel-spring-boot uses server.* / "
+ "(camel.server.* / camel.management.* on camel-main;
camel-spring-boot uses server.* / "
+
"management.* via Spring Boot; camel-quarkus uses
quarkus.http.* / quarkus.management.*). "
+
"Legacy camel.springboot.* keys are rewritten to
camel.main.* (deprecated in 4.5, removed in "
+
- "4.13). Properties without a known translation are
passed through with status=unknown so the "
+ "4.13), except the Spring Boot only options
(camel.springboot.lenient-configuration-binding "
+ +
+ "keeps its name), which have no equivalent on the
other runtimes. "
+ +
+ "Properties without a known translation are passed
through with status=unknown so the "
+
"caller can review them — the tool never guesses.")
public PropertiesTranslateResult camel_properties_translate(
@@ -269,7 +289,23 @@ public class PropertiesTranslateTools {
String value = line.substring(eq + 1);
String key = rawKey.trim();
- // 1. Legacy cleanup: camel.springboot.* is deprecated and removed
since 4.13.
+ // 1. Spring Boot only options: no equivalent on camel-main or Quarkus
+ String springBootOnly = springBootOnlyOption(key);
+ if (springBootOnly != null && (key.startsWith("camel.springboot.") ||
from == Runtime.SPRING_BOOT)) {
+ if (to != Runtime.SPRING_BOOT) {
+ return new TranslatedLine(
+ lineNumber, line, line, "unknown",
+ "Spring Boot only option (" + springBootOnly + "): no
equivalent in " + to.label());
+ }
+ if (springBootOnly.equals(key) ||
!key.startsWith("camel.springboot.")) {
+ return new TranslatedLine(lineNumber, line, line, "unchanged",
null);
+ }
+ return new TranslatedLine(
+ lineNumber, line, springBootOnly + "=" + value,
"translated",
+ "Legacy camel.springboot.* option is " + springBootOnly +
" since Camel 4.13");
+ }
+
+ // 2. Legacy cleanup: camel.springboot.* is deprecated and removed
since 4.13.
// Rewrite to camel.main.* regardless of source/target runtime.
if (key.startsWith("camel.springboot.")) {
String newKey = "camel.main." +
key.substring("camel.springboot.".length());
@@ -279,7 +315,7 @@ public class PropertiesTranslateTools {
"Legacy camel.springboot.* prefix rewritten to
camel.main.* (deprecated since Camel 4.5, removed in 4.13)");
}
- // 2. Known per-runtime physical keys -> look up the canonical, then
re-emit for the target.
+ // 3. Known per-runtime physical keys -> look up the canonical, then
re-emit for the target.
String canonical = KEY_TO_CANONICAL.get(from).get(key);
if (canonical != null) {
if (from == to) {
@@ -296,20 +332,36 @@ public class PropertiesTranslateTools {
return new TranslatedLine(lineNumber, line, translatedLine,
"translated", null);
}
- // 3. Camel-agnostic prefixes are identical across runtimes.
+ // 4. Camel-agnostic prefixes are identical across runtimes.
for (String prefix : CAMEL_AGNOSTIC_PREFIXES) {
if (key.startsWith(prefix)) {
return new TranslatedLine(lineNumber, line, line, "unchanged",
null);
}
}
- // 4. Anything else (server.*, quarkus.datasource.*, spring.jpa.*,
logging.*, etc.) is out of scope —
+ // 5. Anything else (server.*, quarkus.datasource.*, spring.jpa.*,
logging.*, etc.) is out of scope —
// we never guess. Pass through unchanged with a note.
return new TranslatedLine(
lineNumber, line, line, "unknown",
"No translation rule for this key; passed through unchanged");
}
+ /**
+ * The Spring Boot key of a Spring Boot only option written as {@code
camel.springboot.<name>} or
+ * {@code camel.main.<name>} (any relaxed-binding spelling), else null.
+ */
+ private static String springBootOnlyOption(String key) {
+ String name;
+ if (key.startsWith("camel.springboot.")) {
+ name = key.substring("camel.springboot.".length());
+ } else if (key.startsWith("camel.main.")) {
+ name = key.substring("camel.main.".length());
+ } else {
+ return null;
+ }
+ return SPRING_BOOT_ONLY_OPTIONS.get(name.replace("-", "").replace("_",
"").toLowerCase(Locale.ENGLISH));
+ }
+
// Result record classes for Jackson serialization
public record PropertiesTranslateResult(String fromRuntime, String
toRuntime, TranslateSummary summary,
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CamelReleasesTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CamelReleasesTest.java
new file mode 100644
index 000000000000..00ab07182c54
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CamelReleasesTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.commands.mcp;
+
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.ReleaseModel;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CamelReleasesTest {
+
+ private static final CamelCatalog CATALOG = new DefaultCamelCatalog() {
+ @Override
+ public List<ReleaseModel> camelReleases() {
+ return List.of(release("4.22.0", "17,21,25"), release("4.22.1",
"17,21,25"), release("4.23.0", "17,21,25"));
+ }
+ };
+
+ @Test
+ void exactAndSameMinorReleases() {
+ assertThat(CamelReleases.forVersion(CATALOG,
"4.22.0").getVersion()).isEqualTo("4.22.0");
+ // a patch release does not change the JDK baseline: the newest
release of the same minor version tells it
+ assertThat(CamelReleases.forVersion(CATALOG,
"4.22.5").getVersion()).isEqualTo("4.22.1");
+ assertThat(CamelReleases.forVersion(CATALOG,
"4.23.1-SNAPSHOT").getVersion()).isEqualTo("4.23.0");
+ }
+
+ @Test
+ void noReleaseForAVersionNewerThanTheCatalog() {
+ // the catalog only learns a release once it is out, and a new minor
version may drop a JDK version
+ assertThat(CamelReleases.forVersion(CATALOG, "4.24.0")).isNull();
+ assertThat(CamelReleases.forVersion(CATALOG,
"4.24.0-SNAPSHOT")).isNull();
+ assertThat(CamelReleases.forVersion(CATALOG, "5.0.0")).isNull();
+ assertThat(CamelReleases.forVersion(CATALOG, "4.21.0")).isNull();
+ }
+
+ private static ReleaseModel release(String version, String jdk) {
+ ReleaseModel release = new ReleaseModel();
+ release.setVersion(version);
+ release.setJdk(jdk);
+ return release;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
index 27a6a6cecd5a..46227fa9081e 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
@@ -181,10 +181,107 @@ class DependencyCheckToolsTest {
DependencyCheckTools.DependencyCheckResult result
= tools.camel_dependency_check(POM_WITH_BOM, null, null, null,
null, null);
- // 4.10.0 is older than the catalog version (4.24.0-SNAPSHOT)
+ // 4.10.0 is older than the latest release, which is a release and not
the SNAPSHOT catalog version
assertThat(result.versionStatus().status()).isEqualTo("outdated");
assertThat(result.versionStatus().outdated()).isTrue();
assertThat(result.versionStatus().catalogVersion()).isNotEmpty();
+
assertThat(result.versionStatus().latestVersion()).matches("\\d+\\.\\d+\\.\\d+");
+ assertThat(result.recommendations()).filteredOn(r ->
r.category().equals("Version Upgrade"))
+ .singleElement().satisfies(r ->
assertThat(r.action()).doesNotContain("SNAPSHOT"));
+ }
+
+ @Test
+ void runtimeArtifactsProvideTheirComponent() {
+ String route = "from:\n uri: kafka:myTopic\n steps:\n - to:
jms:queue:out";
+ String springBoot =
POM_SPRING_BOOT.replace("<artifactId>camel-spring-boot-starter</artifactId>",
+ "<artifactId>camel-kafka-starter</artifactId>");
+ String quarkus = """
+ <project>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>io.quarkus.platform</groupId>
+ <artifactId>quarkus-camel-bom</artifactId>
+ <version>${quarkus.platform.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-jms</artifactId>
+ <version>3.27.0</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-kafka</artifactId>
+ </dependency>
+ </dependencies>
+ </project>
+ """;
+
+ // camel-kafka-starter and camel-quarkus-kafka are camel-kafka; jms is
missing, also when only managed
+ DependencyCheckTools.DependencyCheckResult sb =
tools.camel_dependency_check(springBoot, route, null, null, null, null);
+ assertThat(sb.missingDependencies()).singleElement().satisfies(d -> {
+ assertThat(d.component()).isEqualTo("jms");
+ assertThat(d.groupId()).isEqualTo("org.apache.camel.springboot");
+ assertThat(d.artifactId()).isEqualTo("camel-jms-starter");
+ });
+
+ DependencyCheckTools.DependencyCheckResult q =
tools.camel_dependency_check(quarkus, route, null, null, null, null);
+ assertThat(q.projectInfo().runtimeType()).isEqualTo("quarkus");
+ assertThat(q.missingDependencies()).singleElement().satisfies(d -> {
+ assertThat(d.groupId()).isEqualTo("org.apache.camel.quarkus");
+ assertThat(d.artifactId()).isEqualTo("camel-quarkus-jms");
+ });
+ }
+
+ @Test
+ void quarkusBomIsNotVersionedLikeCamel() {
+ DependencyData data = new DependencyData();
+
+ assertThat(data.formatBomSnippet("quarkus", "4.22.0", null))
+ .contains("<artifactId>camel-quarkus-bom</artifactId>")
+ .contains("<version>${camel-quarkus.version}</version>")
+ .contains("camel_version_list")
+ .doesNotContain("4.22.0");
+ assertThat(data.formatBomSnippet("quarkus", "4.22.0", "3.27.1"))
+ .contains("<groupId>io.quarkus.platform</groupId>")
+ .contains("<artifactId>quarkus-camel-bom</artifactId>")
+ .contains("<version>3.27.1</version>");
+ // the Camel Quarkus version of the project (camel-quarkus.version or
the camel-quarkus-bom), when known
+ assertThat(data.formatBomSnippet("quarkus", "4.22.0", null, "3.27.0"))
+ .contains("<groupId>org.apache.camel.quarkus</groupId>")
+ .contains("<artifactId>camel-quarkus-bom</artifactId>")
+ .contains("<version>3.27.0</version>")
+ .doesNotContain("camel_version_list");
+ }
+
+ @Test
+ void quarkusBomRecommendationUsesTheCamelQuarkusVersionOfTheProject() {
+ String pom = """
+ <project>
+ <properties>
+ <camel.version>4.14.0</camel.version>
+ <camel-quarkus.version>3.27.0</camel-quarkus.version>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-kafka</artifactId>
+ <version>${camel-quarkus.version}</version>
+ </dependency>
+ </dependencies>
+ </project>
+ """;
+
+ DependencyCheckTools.DependencyCheckResult result =
tools.camel_dependency_check(pom, null, null, null, null, null);
+ assertThat(result.recommendations()).filteredOn(r ->
r.category().equals("Best Practice"))
+ .singleElement().satisfies(r -> assertThat(r.snippet())
+ .contains("<artifactId>camel-quarkus-bom</artifactId>")
+ .contains("<version>3.27.0</version>"));
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditToolsTest.java
index 186958df6dd1..61269fa6772f 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditToolsTest.java
@@ -161,7 +161,60 @@ class DependencySecurityAuditToolsTest {
= tools.camel_dependency_security_audit(SIMPLE_POM, null,
null, null, null, null);
assertThat(result.recommendations()).isNotNull();
- assertThat(result.recommendations()).anyMatch(r ->
r.contains("Upgrade"));
+ assertThat(result.recommendations()).anyMatch(r ->
r.startsWith("Upgrade from Camel 4.10.0"))
+ .noneMatch(r -> r.contains("SNAPSHOT"));
+ }
+
+ @Test
+ void shouldSayWhenTheCamelVersionIsNotDetected() {
+ // a Spring Boot project whose Camel version comes from the parent:
the starter is audited as camel-http
+ String parentManaged = """
+ <project>
+ <parent>
+ <groupId>com.example</groupId>
+ <artifactId>company-parent</artifactId>
+ <version>1.0</version>
+ </parent>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.springboot</groupId>
+ <artifactId>camel-http-starter</artifactId>
+ </dependency>
+ </dependencies>
+ </project>
+ """;
+
+ DependencySecurityAuditTools.AuditResult unknown
+ = tools.camel_dependency_security_audit(parentManaged, null,
null, null, null, null);
+ assertThat(unknown.summary().camelVersion()).isNull();
+ assertThat(unknown.summary().camelVersionDetected()).isFalse();
+ assertThat(unknown.summary().clean()).isFalse();
+
assertThat(unknown.warnings()).singleElement().asString().contains("camelVersion");
+ assertThat(unknown.vulnerableArtifacts()).singleElement().satisfies(a
-> {
+ assertThat(a.artifactId()).isEqualTo("camel-http-starter");
+ assertThat(a.reachable()).as("no routes, reachability
unknown").isNull();
+ });
+ assertThat(unknown.recommendations()).noneMatch(r ->
r.contains("directly used in routes"));
+
+ // the version of this server's catalog, so that no other catalog is
downloaded
+ String version =
tools.catalogService.getDefaultCatalog().getCatalogVersion().replace("-SNAPSHOT",
"");
+ DependencySecurityAuditTools.AuditResult given
+ = tools.camel_dependency_security_audit(parentManaged, null,
null, version, null, null);
+ assertThat(given.summary().camelVersion()).isEqualTo(version);
+ assertThat(given.summary().camelVersionDetected()).isFalse();
+
assertThat(given.warnings()).singleElement().asString().contains(version);
+ }
+
+ @Test
+ void shouldNotBeCleanWhenAdvisoriesWithoutComponentsMayApply() {
+ // CVE-2024-22371 names no component and affects 4.1.0 before 4.4.0
+ DependencySecurityAuditTools.AuditResult result =
tools.camel_dependency_security_audit(
+ CURRENT_VERSION_POM.replace("4.22.0", "4.2.0"), null, null,
null, null, null);
+
+
assertThat(result.uncheckedAdvisories()).extracting(DependencySecurityAuditTools.VulnerabilityFinding::cve)
+ .contains("CVE-2024-22371");
+ assertThat(result.summary().uncheckedAdvisories()).isPositive();
+ assertThat(result.summary().clean()).isFalse();
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java
index 97acfc6bb992..39108501378a 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java
@@ -29,8 +29,61 @@ class MigrationToolsTest {
MigrationToolsTest() {
tools = new MigrationTools();
tools.migrationData = new MigrationData();
+ tools.catalogService = new CatalogService();
}
+ // a Spring Boot application packaged as a WAR for Tomcat: the BOM and a
managed-only component declare nothing,
+ // the plugin dependency is not a project dependency
+ private static final String SPRING_BOOT_WAR_POM = """
+ <project>
+ <packaging>war</packaging>
+ <parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>3.5.0</version>
+ </parent>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.springboot</groupId>
+ <artifactId>camel-spring-boot-bom</artifactId>
+ <version>4.18.0</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.springboot</groupId>
+ <artifactId>camel-jms-starter</artifactId>
+ <version>4.18.0</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.springboot</groupId>
+ <artifactId>camel-kafka-starter</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-war-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-maven-plugin</artifactId>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-xml-io</artifactId>
+ </dependency>
+ </dependencies>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
private static final String CLEAN_POM = """
<project>
<properties>
@@ -98,6 +151,142 @@ class MigrationToolsTest {
assertThat(result.warnings()).noneMatch(w -> w.contains("Sensitive
data detected"));
}
+ // ---- Runtime detection ----
+
+ @Test
+ void springBootWarIsSpringBootAndOnlyDeclaredDependenciesCount() {
+ MigrationTools.ProjectAnalysisResult result =
tools.camel_migration_analyze(SPRING_BOOT_WAR_POM, null);
+
+ assertThat(result.runtimeType()).isEqualTo("spring-boot");
+ assertThat(result.camelVersion()).isEqualTo("4.18.0");
+
assertThat(result.camelComponents()).containsExactly("camel-kafka-starter");
+
assertThat(result.nextStep()).contains("camel_migration_compatibility");
+ }
+
+ @Test
+ void pluginsAreRuntimeMarkers() {
+ String plainWar = """
+ <project>
+
<properties><camel.version>3.20.0</camel.version></properties>
+
<build><plugins><plugin><artifactId>maven-war-plugin</artifactId></plugin></plugins></build>
+ </project>
+ """;
+ String bundle = """
+ <project>
+
<properties><camel.version>3.20.0</camel.version></properties>
+ <build><plugins><plugin>
+
<groupId>org.apache.felix</groupId><artifactId>maven-bundle-plugin</artifactId>
+ </plugin></plugins></build>
+ </project>
+ """;
+
+ assertThat(tools.camel_migration_analyze(plainWar,
null).runtimeType()).isEqualTo("wildfly");
+ assertThat(tools.camel_migration_analyze(bundle,
null).runtimeType()).isEqualTo("karaf");
+ }
+
+ @Test
+ void quarkusSpringBootPropertiesIsNotSpringBoot() {
+ // a Camel Quarkus project with the Quarkus extension for Spring Boot
properties: Spring Boot is told by the group
+ String pom = """
+ <project>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>io.quarkus.platform</groupId>
+ <artifactId>quarkus-camel-bom</artifactId>
+ <version>3.27.1</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-kafka</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+
<artifactId>quarkus-spring-boot-properties</artifactId>
+ </dependency>
+ </dependencies>
+ </project>
+ """;
+
+ assertThat(tools.camel_migration_analyze(pom,
null).runtimeType()).isEqualTo("quarkus");
+ }
+
+ // ---- Compatibility and recipes ----
+
+ @Test
+ void compatibilityTakesTheJavaRequirementFromTheReleaseData() {
+ // the catalog's release data: 4.20 supports Java 17, 21 and 25; 4.0
needs Java 17
+ MigrationTools.CompatibilityResult java17 =
tools.camel_migration_compatibility("4.18.0", "4.20.0", "main", "17");
+ assertThat(java17.compatible()).isTrue();
+
assertThat(java17.javaCompatibility().requiredVersion()).isEqualTo("17");
+
assertThat(java17.javaCompatibility().supportedVersions()).isEqualTo("17, 21,
25");
+
+ MigrationTools.CompatibilityResult java11 =
tools.camel_migration_compatibility("3.20.0", "4.0.0", "main", "11");
+ assertThat(java11.compatible()).isFalse();
+
assertThat(java11.blockers()).singleElement().asString().contains("requires
Java 17+");
+ }
+
+ @Test
+ void compatibilityHasNoJavaRequirementForAVersionNewerThanTheCatalog() {
+ // the catalog only learns a release once it is out: the next minor
version may drop a JDK version (as 4.24
+ // drops Java 17), so the newest release must not tell its Java
requirement
+ String latest =
CamelReleases.latest(tools.catalogService.getDefaultCatalog());
+ String[] parts = latest.split("\\.");
+ String nextMinor = parts[0] + "." + (Integer.parseInt(parts[1]) + 1) +
".0";
+
+ MigrationTools.CompatibilityResult result =
tools.camel_migration_compatibility(latest, nextMinor, "main", "17");
+ assertThat(result.javaCompatibility()).isNull();
+ assertThat(result.blockers()).isEmpty();
+ assertThat(result.warnings()).singleElement().asString()
+ .contains("no release data for Camel " +
nextMinor).contains("camel_version_list");
+
+ // a patch release of a known minor version takes the Java requirement
of that minor version
+ String patch = parts[0] + "." + parts[1] + ".99";
+ MigrationTools.CompatibilityResult samePatch =
tools.camel_migration_compatibility(latest, patch, "main", "17");
+ assertThat(samePatch.javaCompatibility()).isNotNull();
+ assertThat(samePatch.warnings()).noneMatch(w -> w.contains("no release
data"));
+ }
+
+ @Test
+ void recipesUseTheRecipeOfEachArtifact() {
+ MigrationTools.MigrationRecipesResult main
+ = tools.camel_migration_recipes("main", "4.14.0", "4.22.1",
"17", null, null);
+ assertThat(main.mavenCommands()).singleElement().asString()
+ .contains("rewrite-maven-plugin:dryRun ")
+
.contains("org.apache.camel.upgrade:camel-upgrade-recipes:4.22.0 ")
+
.endsWith("-Drewrite.activeRecipes=org.apache.camel.upgrade.CamelMigrationRecipe");
+ assertThat(main.javaUpgradeSuggestions()).isEmpty();
+
+ MigrationTools.MigrationRecipesResult springBoot
+ = tools.camel_migration_recipes("spring-boot", "4.14.0",
"4.22.0", "11", false, null);
+ assertThat(springBoot.mavenCommands()).singleElement().asString()
+ .contains("rewrite-maven-plugin:run ")
+
.contains("org.apache.camel.upgrade:camel-spring-boot-upgrade-recipes:4.22.0 ")
+
.endsWith("-Drewrite.activeRecipes=org.apache.camel.upgrade.CamelSpringBootMigrationRecipe");
+
assertThat(springBoot.javaUpgradeSuggestions()).singleElement().asString()
+ .contains("requires Java 17+").contains("UpgradeToJava17");
+ }
+
+ @Test
+ void quarkusRecipesUseTheQuarkusPlatformVersion() {
+ MigrationTools.MigrationRecipesResult pinned
+ = tools.camel_migration_recipes("quarkus", "4.14.0", "4.22.0",
null, false, "3.27.1");
+ assertThat(pinned.mavenCommands()).singleElement().asString()
+
.contains("io.quarkus.platform:quarkus-maven-plugin:3.27.1:update
-DplatformVersion=3.27.1 -Drewrite=true");
+
+ MigrationTools.MigrationRecipesResult latest
+ = tools.camel_migration_recipes("quarkus", "4.14.0", "4.22.0",
null, null, null);
+ assertThat(latest.mavenCommands()).singleElement().asString()
+ .contains("io.quarkus.platform:quarkus-maven-plugin:update
-DrewriteDryRun=true")
+ .doesNotContain("4.22.0");
+ assertThat(latest.notes()).anyMatch(n ->
n.contains("camel_version_list") && n.contains("quarkusPlatformVersion"));
+ }
+
// ---- Guide search ----
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java
index bc605edd1199..405e9b0407a7 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java
@@ -140,6 +140,10 @@ class PromptDefinitionsTest {
assertThat(text).contains("camel_migration_wildfly_karaf");
assertThat(text).contains("camel_migration_recipes");
assertThat(text).contains("camel_migration_guide_search");
+ // the arguments the tools take: camel_migration_compatibility has no
components argument
+ assertThat(text).contains(
+ "`camel_migration_compatibility` with `currentVersion`,
`targetVersion`, `runtime`, and `javaVersion`.");
+
assertThat(text).contains("`quarkusPlatformVersion`").contains("`runtime=quarkus`");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
index 4f5703e3d305..42ca2adf9c7d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
@@ -111,6 +111,22 @@ class PropertiesTranslateToolsTest {
assertThat(result.properties().get(1).translated()).isEqualTo("camel.main.streamCachingEnabled=true");
}
+ @Test
+ void springBootOnlyOptionsKeepTheirSpringBootKey() {
+ PropertiesTranslateTools.PropertiesTranslateResult springBoot =
tools.camel_properties_translate(
+
"camel.springboot.lenient-configuration-binding=true\ncamel.springboot.main-run-controller=true",
+ "spring-boot", "spring-boot");
+
assertThat(springBoot.properties().get(0).status()).isEqualTo("unchanged");
+
assertThat(springBoot.properties().get(1).translated()).isEqualTo("camel.main.run-controller=true");
+
+ PropertiesTranslateTools.PropertiesTranslateResult main =
tools.camel_properties_translate(
+
"camel.springboot.lenient-configuration-binding=true\ncamel.main.include-non-singletons=true",
+ "spring-boot", "main");
+ assertThat(main.summary().unknown()).isEqualTo(2);
+ assertThat(main.properties().get(0).note()).contains("Spring Boot
only");
+
assertThat(main.properties().get(1).translated()).isEqualTo("camel.main.include-non-singletons=true");
+ }
+
@Test
void legacySpringbootPrefixIsRewrittenEvenWhenTargetIsQuarkus() {
PropertiesTranslateTools.PropertiesTranslateResult result =
tools.camel_properties_translate(