You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/15 18:12:36 UTC

(camel) 01/01: CAMEL-24708: camel-groovy - a message script variable, the Camel 4 name of request, and a missing property hint that only names variables the binding has

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-24708-groovy-message
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 7dc6eeea9919ce89a9a92841fa320875bdcf3dc9
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue Sep 15 20:12:22 2026 +0200

    CAMEL-24708: camel-groovy - a message script variable, the Camel 4 name of request, and a missing property hint that only names variables the binding has
    
    Every other script language (javascript, python, python3, joor) binds
    message; groovy only had the template-era request and in. The
    MissingPropertyException hint of CAMEL-24698 listed message although it
    did not exist, so a script that followed the hint failed again. The hint
    now comes from a constant checked against the binding's variable set,
    and the groovy card of camel_catalog_doc kind=api says message with
    request and in as its aliases.
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../apache/camel/catalog/docs/groovy-language.adoc |  3 ++-
 .../src/main/docs/groovy-language.adoc             |  3 ++-
 .../camel/language/groovy/GroovyExpression.java    | 24 +++++++++++++++-------
 .../groovy/GroovyExpressionBindingTest.java        | 15 ++++++++++++--
 .../groovy/GroovyMissingPropertyHintTest.java      |  8 ++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  5 +++++
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    | 13 ++++++------
 .../jbang/core/commands/ai/CatalogDocsTest.java    |  9 ++++----
 8 files changed, 58 insertions(+), 22 deletions(-)

diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc
index cbbaf920ace3..6dc429ec1ee3 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc
@@ -53,7 +53,8 @@ a `Map`). The `Exchange` is transferred as:
 |`camelContext` |The Camel Context.
 |`exception` | If the exchange failed then this is the caused exception.
 
-|`request` |The message.
+|`message` |The message (`exchange.getMessage()`).
+|`request` |The message (the older name of `message`).
 |`response` | *Deprecated* The Out message (only for InOut message exchange pattern).
 
 |`attachments` | A `Map<String,jakarta.activation.DataHandler>` containing file attachments such as from HTTP file uploads, or emails containing files.
diff --git a/components/camel-groovy/src/main/docs/groovy-language.adoc b/components/camel-groovy/src/main/docs/groovy-language.adoc
index cbbaf920ace3..6dc429ec1ee3 100644
--- a/components/camel-groovy/src/main/docs/groovy-language.adoc
+++ b/components/camel-groovy/src/main/docs/groovy-language.adoc
@@ -53,7 +53,8 @@ a `Map`). The `Exchange` is transferred as:
 |`camelContext` |The Camel Context.
 |`exception` | If the exchange failed then this is the caused exception.
 
-|`request` |The message.
+|`message` |The message (`exchange.getMessage()`).
+|`request` |The message (the older name of `message`).
 |`response` | *Deprecated* The Out message (only for InOut message exchange pattern).
 
 |`attachments` | A `Map<String,jakarta.activation.DataHandler>` containing file attachments such as from HTTP file uploads, or emails containing files.
diff --git a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
index ee22fb79d927..0bde520fe6a1 100644
--- a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
+++ b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
@@ -44,6 +44,15 @@ public class GroovyExpression extends ExpressionSupport {
 
     private static final Logger LOG = LoggerFactory.getLogger(GroovyExpression.class);
 
+    /**
+     * The script variables named in the missing property hint: the ones a script reaches for, without the aliases
+     * (header, variable, in, exchangeProperty) and the out message that only exists on an InOut exchange. Every name
+     * must be in {@link ExchangeBinding#EXCHANGE_VARIABLES}.
+     */
+    static final String SCRIPT_VARIABLES_HINT
+            = "exchange, message, body, headers, variables, exchangeProperties, exception, camelContext, attachments"
+              + " and log";
+
     private final String text;
 
     // the language and shell factory of the CamelContext, resolved once instead of on every evaluation
@@ -90,8 +99,7 @@ public class GroovyExpression extends ExpressionSupport {
                     ? "'" + name + "' is a bean in the registry, not a script variable; use"
                       + " exchange.getContext().getRegistry().lookupByName('" + name + "'), or call it from the route with"
                       + " - bean: {ref: " + name + "}"
-                    : "the script variables are exchange, message, body, headers, variables, exchangeProperties,"
-                      + " camelContext, request and log";
+                    : "the script variables are " + SCRIPT_VARIABLES_HINT;
             throw new groovy.lang.MissingPropertyException(
                     e.getMessageWithoutLocationText() + " (" + hint + ")", name, e.getType());
         }
@@ -180,7 +188,7 @@ public class GroovyExpression extends ExpressionSupport {
 
     /**
      * Binding with the same variables as {@link ExchangeHelper#populateVariableMap(Exchange, Map, boolean)} plus
-     * attachments and log.
+     * message (the current message, the Camel 4 name of request), attachments and log.
      * <p>
      * The body, the headers, the exception and the out message are read when the binding is created. The values that
      * are costly to create (the copy of the exchange properties, the variable repository and the attachment message)
@@ -191,11 +199,12 @@ public class GroovyExpression extends ExpressionSupport {
      * A global variable of the {@link GroovyShellFactory} is hidden by the exchange variable with the same name, except
      * {@code out} and {@code response} when the exchange has no out message, as they are then not exposed.
      */
-    private static final class ExchangeBinding extends Binding {
+    static final class ExchangeBinding extends Binding {
 
-        private static final Set<String> EXCHANGE_VARIABLES = Set.of(
-                "body", "header", "headers", "variable", "variables", "exception", "in", "request", "exchange",
-                "exchangeProperty", "exchangeProperties", "out", "response", "camelContext", "attachments", "log");
+        static final Set<String> EXCHANGE_VARIABLES = Set.of(
+                "body", "header", "headers", "variable", "variables", "exception", "in", "request", "message",
+                "exchange", "exchangeProperty", "exchangeProperties", "out", "response", "camelContext", "attachments",
+                "log");
 
         private final Exchange exchange;
         private final Message in;
@@ -297,6 +306,7 @@ public class GroovyExpression extends ExpressionSupport {
                     return exception;
                 case "in":
                 case "request":
+                case "message":
                     return in;
                 case "exchange":
                     return exchange;
diff --git a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyExpressionBindingTest.java b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyExpressionBindingTest.java
index 50f3ced73ed5..bdd84e7d18e4 100644
--- a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyExpressionBindingTest.java
+++ b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyExpressionBindingTest.java
@@ -77,6 +77,8 @@ public class GroovyExpressionBindingTest {
         assertEquals("myValue", evaluate("exchangeProperties.myProperty"));
         assertSame(exchange, evaluate("exchange"));
         assertSame(exchange.getIn(), evaluate("request"));
+        assertSame(exchange.getMessage(), evaluate("message"));
+        assertEquals("James", evaluate("message.getHeader('name')"));
         assertSame(context, evaluate("camelContext"));
         assertEquals(Boolean.TRUE, evaluate("attachments.isEmpty()"));
         assertEquals(Boolean.TRUE, evaluate("log != null"));
@@ -103,12 +105,21 @@ public class GroovyExpressionBindingTest {
         assertEquals("World", exchange.getIn().getBody());
     }
 
+    @Test
+    public void testEveryVariableTheHintNamesExists() {
+        // the hint is hand-written; a name it lists must be one the binding exposes, so the next attempt succeeds
+        for (String name : GroovyExpression.SCRIPT_VARIABLES_HINT.replace(" and ", ", ").split(", ")) {
+            assertTrue(GroovyExpression.ExchangeBinding.EXCHANGE_VARIABLES.contains(name),
+                    "the hint names '" + name + "' but the binding does not expose it");
+        }
+    }
+
     @Test
     public void testBindingVariables() {
         assertEquals(Boolean.TRUE, evaluate(
                 "binding.variables.keySet().containsAll(['body', 'header', 'headers', 'variable', 'variables', 'exception',"
-                                            + " 'in', 'request', 'exchange', 'exchangeProperty', 'exchangeProperties',"
-                                            + " 'camelContext', 'attachments', 'log'])"));
+                                            + " 'in', 'request', 'message', 'exchange', 'exchangeProperty',"
+                                            + " 'exchangeProperties', 'camelContext', 'attachments', 'log'])"));
         assertEquals("World", evaluate("binding.variables.body"));
         assertEquals("Bye", evaluate("body = 'Bye'; binding.variables.body"));
     }
diff --git a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
index 59b3dc6d6c4e..dd9f76b72f85 100644
--- a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
+++ b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
@@ -22,6 +22,7 @@ import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -50,6 +51,12 @@ public class GroovyMissingPropertyHintTest extends CamelTestSupport {
                 cause.getMessage());
     }
 
+    @Test
+    public void messageIsAScriptVariableAsTheHintSays() {
+        String out = template.requestBodyAndHeader("direct:message", "World", "name", "Hello", String.class);
+        assertEquals("Hello World", out);
+    }
+
     @Override
     protected RoutesBuilder createRouteBuilder() {
         return new RouteBuilder() {
@@ -57,6 +64,7 @@ public class GroovyMissingPropertyHintTest extends CamelTestSupport {
             public void configure() {
                 from("direct:bean").transform().groovy("formatter.append(body)");
                 from("direct:unknown").transform().groovy("nosuch.toUpperCase()");
+                from("direct:message").transform().groovy("message.getHeader('name') + ' ' + message.body");
             }
         };
     }
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 ed31c839a516..d093fa8f0c9a 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
@@ -116,6 +116,11 @@ from the exchange the first time the script uses them instead of being copied be
 changes a property or variable through `exchange` and then reads one of these variables for the first time now sees
 its own change where it previously saw the state from before the script ran.
 
+Groovy scripts now have a `message` variable for the current message (`exchange.getMessage()`), the name the other
+script languages and the Camel 4 API use; `request` and `in` stay as its older names. A `GroovyShellFactory` that
+provided its own global variable named `message` is now hidden by the exchange variable, like the other exchange
+variable names.
+
 === camel-dynamic-router
 
 The `dynamic-router-control` endpoint no longer takes the subscription `predicate`, or the
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 3462f80ad94e..041cb38f771b 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -471,7 +471,7 @@ public final class CatalogDocs {
 
     /**
      * The variables a script language binds, hand-written because each language binds its own set with its own names
-     * (groovy has camelContext and no message, javascript has context and message), and how a script reaches the Camel
+     * (groovy has camelContext and request, javascript has context and no request), and how a script reaches the Camel
      * API and a registry bean from them; null for a language that is not a script.
      */
     static JsonObject scriptVariables(String language) {
@@ -480,7 +480,8 @@ public final class CatalogDocs {
         switch (language) {
             case "groovy" -> {
                 variables.put("exchange", "the Exchange");
-                variables.put("request", "the message (exchange.getMessage()); in is an alias");
+                variables.put("message",
+                        "the message (exchange.getMessage()); request is an alias, in is an alias (older names)");
                 variables.put("body", "the message body");
                 variables.put("headers", "the message headers (Map); header is an alias");
                 variables.put("variables", "the exchange variables (Map); variable is an alias");
@@ -490,12 +491,10 @@ public final class CatalogDocs {
                 variables.put("attachments", "the message attachments (Map)");
                 variables.put("log", "an SLF4J logger");
                 variables.put("response", "the out message, only when one exists; out is an alias");
-                note = "There is no message variable: use request (or exchange.message). The value of the last"
-                       + " statement is the result. A registry bean is not a variable: use"
+                note = "The value of the last statement is the result. A registry bean is not a variable: use"
                        + " camelContext.registry.lookupByName('myBean'). Setting body or headers in the script"
-                       + " does not change the message: use exchange.message.body = ... or exchange.message"
-                       + ".setHeader(name, value). Groovy property syntax works on the Camel API: exchange.message.body,"
-                       + " exchange.context.registry.";
+                       + " does not change the message: use message.body = ... or message.setHeader(name, value)."
+                       + " Groovy property syntax works on the Camel API: message.body, exchange.context.registry.";
             }
             case "js", "python" -> {
                 variables.put("exchange", "the Exchange");
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
index 521fc8f9ba3f..adfb3680cba3 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
@@ -276,8 +276,8 @@ class CatalogDocsTest {
         JsonObject groovy = catalogDoc(Map.of("name", "groovy", "kind", "api"));
         assertEquals("api", groovy.getString("kind"));
         JsonObject vars = groovy.getMap("variables");
-        assertTrue(vars.containsKey("camelContext") && vars.containsKey("request") && vars.containsKey("log"));
-        assertFalse(vars.containsKey("message"), "groovy binds no message variable");
+        assertTrue(vars.containsKey("camelContext") && vars.containsKey("message") && vars.containsKey("log"));
+        assertTrue(vars.getString("message").contains("request"), "the older name is given as an alias");
         assertTrue(groovy.getString("note").contains("lookupByName('myBean')"), "a bean name is not a variable");
         assertTrue(groovy.getCollection("apis").contains("Exchange"));
 
@@ -294,7 +294,7 @@ class CatalogDocsTest {
         JsonObject lang = catalogDoc(Map.of("name", "groovy", "kind", "language"));
         assertEquals("language", lang.getString("kind"));
         assertTrue(lang.getMap("scriptVariables").containsKey("camelContext"));
-        assertTrue(lang.getString("scriptNote").contains("no message variable"));
+        assertTrue(lang.getString("scriptNote").contains("lookupByName('myBean')"));
         assertNull(catalogDoc(Map.of("name", "simple", "kind", "language")).get("scriptVariables"));
     }
 
@@ -316,7 +316,8 @@ class CatalogDocsTest {
                 }
             }
             JsonObject groovy = CatalogDocs.scriptVariables("groovy").getMap("variables");
-            assertTrue(groovy.containsKey("attachments") && groovy.containsKey("log"), "the groovy extras");
+            assertTrue(groovy.containsKey("message") && groovy.containsKey("attachments") && groovy.containsKey("log"),
+                    "the groovy extras");
         }
     }
 }