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 af42f8088895 CAMEL-24708: camel-groovy - a message script variable, 
the Camel 4 name of request (#26478)
af42f8088895 is described below

commit af42f8088895a983b8936a15868442e14e75abc9
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 15 22:52:30 2026 +0200

    CAMEL-24708: camel-groovy - a message script variable, the Camel 4 name of 
request (#26478)
    
    * 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) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-24708: camel-jbang - the script variables javadoc names a 
difference that still holds now groovy has message too
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../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    | 16 +++++++--------
 .../jbang/core/commands/ai/CatalogDocsTest.java    |  9 ++++----
 8 files changed, 60 insertions(+), 23 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 6d14e5e81d19..939d694c7c92 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 866210e7eaeb..32b315d6973c 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
@@ -469,8 +469,9 @@ 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
-     * API and a registry bean from them; null for a language that is not a 
script.
+     * (the CamelContext is camelContext in groovy and context in javascript, 
the exchange properties exchangeProperties
+     * and properties), 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) {
         JsonObject variables = new JsonObject();
@@ -478,7 +479,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");
@@ -488,12 +490,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");
         }
     }
 }

Reply via email to