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");
}
}
}