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 539ad74a4a98 CAMEL-24907: camel-jbang - camel_eval_expression 
evaluates any language, not only the ones on its own classpath (#26753)
539ad74a4a98 is described below

commit 539ad74a4a98333d61a4cbaa99f3ba194818c15e
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 11:22:12 2026 +0200

    CAMEL-24907: camel-jbang - camel_eval_expression evaluates any language, 
not only the ones on its own classpath (#26753)
    
    The component of a language such as jsonpath, jq or xpath is downloaded on 
first use, as camel run downloads what a route needs, and the answer names it 
in downloaded. A failing expression also carries the catalog's syntax error and 
the position where it breaks.
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   5 +
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   6 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |   7 +-
 .../core/commands/ai/ExpressionEvaluator.java      | 145 +++++++++++++++++++--
 .../ai/ExpressionEvaluatorLanguageTest.java        |  70 ++++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |   5 +-
 6 files changed, 221 insertions(+), 17 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index f92b5daa8346..c91514cf01c6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -900,6 +900,11 @@ and what to put there, and validates and reloads the 
result as a write does. A m
 change one step corrupts the lines it did not mean to touch, so this is the 
tool for a change to an existing file;
 `camel_write_file` writes a new one.
 
+`camel_eval_expression` evaluates any language, not only the ones on the 
server's own classpath: the component of
+a language such as `jsonpath`, `jq` or `xpath` is downloaded on first use, as 
`camel run` downloads what a route
+needs, and the answer names it in `downloaded`. A failing expression also 
carries the catalog's syntax error and
+the position where it breaks.
+
 The `camel_run` tool, when no files are named, starts the project with `camel 
run --source-dir=.` instead of
 listing the directory's files: the directory is watched, so a file added 
afterwards (a bean file, a Java class
 under `src/main/java`) is part of the app and reloaded in dev mode, and a 
`restart` starts the same way. Naming
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 8cf2d973f870..7dfeda56011f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -365,8 +365,10 @@ project `directory` as an argument, the runtime tools take 
the integration `name
 
 | `camel_eval_expression`
 | Evaluates an expression (simple by default) in the running integration, or 
locally when none is named,
-  and returns the value (true/false for a predicate) or the syntax error, so 
an agent can check a simple
-  expression before writing it into a route.
+  and returns the value (true/false for a predicate) or the syntax error with 
the position where it breaks,
+  so an agent can check an expression before writing it into a route. Any 
language works: one that is not on
+  the server's own classpath, `jsonpath`, `jq`, `xpath`, `groovy`, has its 
component downloaded on first use
+  and the answer says so in `downloaded`.
 
 | `camel_dependency_for_class`
 | Which Maven dependency provides a class, and how to declare it. Local first: 
the known dependencies
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 36adb55dd749..478fbcd3062c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -307,9 +307,10 @@ public final class AuthoringTools {
                 }));
 
         registry.accept(tool("camel_eval_expression",
-                "Evaluates an expression: in the running integration when 
there is one, else locally. Returns the "
-                                                      + "value (true/false for 
a predicate) or the syntax error, so check simple "
-                                                      + "before answering or 
writing it.")
+                "Evaluates an expression: in the running integration when 
there is one, else locally, in any "
+                                                      + "language (jsonpath, 
jq, xpath, groovy: its component is downloaded when "
+                                                      + "needed). Returns the 
value (true/false for a predicate) or the syntax "
+                                                      + "error, so check an 
expression before answering or writing it.")
                 .param("expression", "string", "e.g. ${random(1,10)} or 
${body} ?: 'none'", true)
                 .param("language", "string", "simple (default), jsonpath, 
xpath, jq", false)
                 .param("body", "string", "Message body", false)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
index 3a532da5be1c..ec4e5800740e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
@@ -16,14 +16,20 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Pattern;
 
 import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.Predicate;
+import org.apache.camel.catalog.LanguageValidationResult;
 import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.main.download.DependencyDownloaderClassLoader;
+import org.apache.camel.main.download.MavenDependencyDownloader;
 import org.apache.camel.spi.Language;
 import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.tooling.model.LanguageModel;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 
@@ -75,7 +81,7 @@ public final class ExpressionEvaluator {
             evaluateInProcess(ctx, lang, expression, body, predicate, result);
         } else {
             result.put("evaluatedIn", "a local scratch context (no integration 
selected)");
-            evaluateLocally(lang, expression, body, predicate, result);
+            evaluateLocally(ctx, lang, expression, body, predicate, result);
         }
         return result;
     }
@@ -113,16 +119,58 @@ public final class ExpressionEvaluator {
     }
 
     private static void evaluateLocally(
-            String lang, String expression, String body, boolean predicate, 
JsonObject result) {
+            ToolContext ctx, String lang, String expression, String body, 
boolean predicate, JsonObject result) {
+        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+        try {
+            String gav = languageArtifact(ctx, lang);
+            // the languages of this process (simple, constant, header, ...) 
need no class loader of their own
+            ClassLoader known = gav != null ? DOWNLOADED.get(gav) : null;
+            if (evaluateWith(known, lang, expression, body, predicate, 
result)) {
+                if (known != null) {
+                    // downloaded by an earlier call: say so again, so the 
answers of two calls read the same
+                    result.put("downloaded", gav);
+                }
+                syntaxCheck(ctx, known, lang, expression, predicate, result);
+                return;
+            }
+            // the language is not on this process's classpath: download its 
component as camel run does, so a
+            // jsonpath, jq or xpath expression can be tried before it is 
written (CAMEL-24907)
+            StringBuilder failure = new StringBuilder();
+            ClassLoader downloaded = download(gav, failure);
+            if (downloaded == null || !evaluateWith(downloaded, lang, 
expression, body, predicate, result)) {
+                result.put("status", "error");
+                result.put("error", "The language '" + lang + "' is not on the 
classpath of this process"
+                                    + (gav != null ? " and " + gav + " could 
not be downloaded" : "")
+                                    + (failure.isEmpty() ? "" : " (" + failure 
+ ")")
+                                    + "; select a running integration that has 
it (camel_run, then its name) and the"
+                                    + " expression is evaluated there");
+                return;
+            }
+            result.put("downloaded", gav);
+            syntaxCheck(ctx, downloaded, lang, expression, predicate, result);
+        } finally {
+            Thread.currentThread().setContextClassLoader(tccl);
+        }
+    }
+
+    /**
+     * Evaluates with the given class loader (null for this process's own), 
and answers whether the language was found;
+     * the result of the evaluation, or its error, is put in {@code result}.
+     */
+    private static boolean evaluateWith(
+            ClassLoader loader, String lang, String expression, String body, 
boolean predicate, JsonObject result) {
         try (DefaultCamelContext context = new DefaultCamelContext(false)) {
+            if (loader != null) {
+                // before the start: the language resolver reads the class 
loader as the context comes up
+                context.setApplicationContextClassLoader(loader);
+                Thread.currentThread().setContextClassLoader(loader);
+            }
             context.start();
             Language language;
             try {
                 language = context.resolveLanguage(lang);
             } catch (Exception e) {
-                result.put("status", "error");
-                result.put("error", "Unknown language '" + lang + "': " + 
e.getMessage());
-                return;
+                return false;
             }
             Exchange exchange = new DefaultExchange(context);
             exchange.getMessage().setBody(body != null ? body : "");
@@ -140,12 +188,89 @@ public final class ExpressionEvaluator {
             result.put("result", value != null ? value.toString() : null);
         } catch (Exception e) {
             result.put("status", "error");
-            Throwable cause = e;
-            while (cause.getCause() != null && cause.getCause() != cause) {
-                cause = cause.getCause();
+            result.put("error", rootCause(e));
+        }
+        return true;
+    }
+
+    /**
+     * The catalog's own check of the text, which names where the syntax 
breaks (its index), so an error says more than
+     * the evaluation's exception; only added when it finds something the 
evaluation did not.
+     */
+    private static void syntaxCheck(
+            ToolContext ctx, ClassLoader loader, String lang, String 
expression, boolean predicate, JsonObject result) {
+        if ("ok".equals(result.getString("status"))) {
+            return;
+        }
+        try {
+            LanguageValidationResult check = predicate
+                    ? ctx.catalog().validateLanguagePredicate(loader, lang, 
expression)
+                    : ctx.catalog().validateLanguageExpression(loader, lang, 
expression);
+            if (!check.isSuccess()) {
+                String error = check.getShortError() != null ? 
check.getShortError() : check.getError();
+                if (error != null) {
+                    result.put("syntaxError", error);
+                    if (check.getIndex() >= 0) {
+                        result.put("syntaxErrorAt", check.getIndex());
+                    }
+                }
             }
-            String message = cause.getMessage();
-            result.put("error", message != null ? message : cause.toString());
+        } catch (Exception e) {
+            // the catalog cannot check this language here; the evaluation's 
own error stands
+        }
+    }
+
+    private static String rootCause(Throwable e) {
+        Throwable cause = e;
+        while (cause.getCause() != null && cause.getCause() != cause) {
+            cause = cause.getCause();
+        }
+        return cause.getMessage() != null ? cause.getMessage() : 
cause.toString();
+    }
+
+    /** The groupId:artifactId:version of a language, from the catalog, or 
null when the catalog does not know it. */
+    private static String languageArtifact(ToolContext ctx, String lang) {
+        try {
+            LanguageModel model = ctx.catalog().languageModel(lang);
+            if (model != null && model.getArtifactId() != null) {
+                return model.getGroupId() + ":" + model.getArtifactId() + ":" 
+ model.getVersion();
+            }
+        } catch (Exception e) {
+            // the catalog does not know it
         }
+        return null;
     }
+
+    /**
+     * Downloads the component of a language and keeps its class loader, so 
the next call does not download again.
+     * Returns null when there is nothing to download (an unknown name) or the 
download fails, appending the reason to
+     * {@code failure}.
+     */
+    private static ClassLoader download(String gav, StringBuilder failure) {
+        if (gav == null) {
+            return null;
+        }
+        String[] parts = gav.split(":");
+        // computeIfAbsent so two evaluations of the same language do not 
download it twice and leak a class loader;
+        // a failed download stores nothing, so the next call tries again
+        return DOWNLOADED.computeIfAbsent(gav, k -> {
+            try {
+                DependencyDownloaderClassLoader cl
+                        = new 
DependencyDownloaderClassLoader(ExpressionEvaluator.class.getClassLoader());
+                try (MavenDependencyDownloader downloader = new 
MavenDependencyDownloader()) {
+                    downloader.setClassLoader(cl);
+                    downloader.start();
+                    downloader.downloadDependency(parts[0], parts[1], 
parts[2]);
+                }
+                return cl;
+            } catch (Exception e) {
+                // say why, so the answer names the cause (offline, wrong 
repository, unknown artifact)
+                failure.append(e.getMessage() != null ? e.getMessage() : 
e.getClass().getSimpleName());
+                return null;
+            }
+        });
+    }
+
+    /** The class loaders of the languages downloaded so far, so the next call 
does not download again. */
+    private static final Map<String, ClassLoader> DOWNLOADED = new 
ConcurrentHashMap<>();
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
new file mode 100644
index 000000000000..076e5ae24910
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.ai;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24907: a language that is not on this process's classpath is 
downloaded, so jsonpath, jq and the others can be
+ * evaluated before an integration runs. The download needs a network or a 
filled local repository, so the test that
+ * downloads runs with -Dcamel.test.download=true.
+ */
+class ExpressionEvaluatorLanguageTest {
+
+    private static final String STOCK = 
"[{\"sku\":\"CAMEL-MUG\",\"qty\":42},{\"sku\":\"CAMEL-CAP\",\"qty\":0}]";
+
+    @Test
+    void aLanguageThatCannotBeLoadedSaysWhatToDo() {
+        JsonObject result = ExpressionEvaluator.evaluate(new ToolContext(), 
"no-such-language", "x", null);
+
+        assertThat(result.getString("status")).isEqualTo("error");
+        assertThat(result.getString("error")).contains("is not on the 
classpath of this process")
+                .contains("select a running integration that has it");
+    }
+
+    @Test
+    void theLanguagesOfThisProcessAreEvaluatedAsBefore() {
+        JsonObject result = ExpressionEvaluator.evaluate(new ToolContext(), 
"simple", "${body}", STOCK);
+
+        assertThat(result.getString("status")).isEqualTo("ok");
+        assertThat(result.getString("result")).isEqualTo(STOCK);
+        assertThat(result.get("downloaded")).isNull();
+    }
+
+    @Test
+    void aBrokenSimpleExpressionCarriesTheSyntaxErrorWithItsPosition() {
+        JsonObject result = ExpressionEvaluator.evaluate(new ToolContext(), 
"simple", "${body", null);
+
+        assertThat(result.getString("status")).isEqualTo("error");
+        assertThat(result.getString("syntaxError")).isNotBlank();
+    }
+
+    @Test
+    @EnabledIfSystemProperty(named = "camel.test.download", matches = "true")
+    void jsonpathIsDownloadedAndEvaluated() {
+        JsonObject result = ExpressionEvaluator.evaluate(new ToolContext(), 
"jsonpath",
+                "$[?(@.sku == 'CAMEL-MUG')]", STOCK);
+
+        assertThat(result.getString("downloaded")).contains("camel-jsonpath");
+        assertThat(result.getString("status")).isEqualTo("ok");
+        assertThat(result.getString("result")).contains("CAMEL-MUG");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index d666d660d8a0..2a11c2405768 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -212,8 +212,9 @@ public class AuthoringTools {
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "Evaluates an expression: in the running integration 
when there is one, else locally. "
-                        + "Returns the value (true/false for a predicate) or 
the syntax error, so check simple before "
+          description = "Evaluates an expression: in the running integration 
when there is one, else locally, in any "
+                        + "language (jsonpath, jq, xpath, groovy: its 
component is downloaded when needed). Returns "
+                        + "the value (true/false for a predicate) or the 
syntax error, so check an expression before "
                         + "answering or writing it.")
     public JsonObject camel_eval_expression(
             @ToolArg(description = "e.g. ${random(1,10)} or ${body} ?: 
'none'", required = true) String expression,

Reply via email to