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 dcdb867e96bb CAMEL-24467: Scripting languages - bind Exchange
variables in JavaScript, MVEL, OGNL, SpEL, Python and Python3, and use the
Groovy binding names in JavaScript, Python and Python3 (#27525)
dcdb867e96bb is described below
commit dcdb867e96bb897d97c6c30e8c40b0d2d233cf94
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 8 22:24:58 2026 +0530
CAMEL-24467: Scripting languages - bind Exchange variables in JavaScript,
MVEL, OGNL, SpEL, Python and Python3, and use the Groovy binding names in
JavaScript, Python and Python3 (#27525)
Groovy and Jactl bind Exchange variables as "variable" and "variables", but
the other scripting languages did not, so a script could only reach
variables
through the exchange object (and not at all in python3's default data-only
mode).
- js and python: bind "variable" and "variables", and use the Groovy binding
names (exchangeProperties, camelContext); the old names are deprecated.
- mvel, ognl and spel (RootObject): add getVariables(), getVariable(name)
and
getVariable(name, type); getVariable resolves global:/route: variables.
- python3 (GraalPy): bind "variables" as a data binding like "headers", in
default and trusted mode, and only the Groovy names (new in 4.23.0, so no
deprecated aliases). Host access is not widened.
The language docs list the new bindings and the upgrade guide notes the
deprecated js/python names.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/js-language.adoc | 22 +++-
.../apache/camel/catalog/docs/mvel-language.adoc | 16 ++-
.../apache/camel/catalog/docs/ognl-language.adoc | 16 ++-
.../apache/camel/catalog/docs/python-language.adoc | 22 +++-
.../camel/catalog/docs/python3-language.adoc | 40 ++++--
.../apache/camel/catalog/docs/spel-language.adoc | 16 ++-
.../src/main/docs/js-language.adoc | 22 +++-
.../camel/language/js/JavaScriptExpression.java | 29 ++++-
.../language/js/JavaScriptBindingNamesTest.java | 69 +++++++++++
.../camel/language/js/JavaScriptVariablesTest.java | 100 +++++++++++++++
.../camel-mvel/src/main/docs/mvel-language.adoc | 16 ++-
.../org/apache/camel/language/mvel/RootObject.java | 25 ++++
.../camel/language/mvel/MvelVariablesTest.java | 91 ++++++++++++++
.../camel-ognl/src/main/docs/ognl-language.adoc | 16 ++-
.../org/apache/camel/language/ognl/RootObject.java | 25 ++++
.../camel/language/ognl/OgnlVariablesTest.java | 91 ++++++++++++++
.../src/main/docs/python-language.adoc | 22 +++-
.../camel/language/python/PythonExpression.java | 29 ++++-
.../language/python/PythonBindingNamesTest.java | 67 ++++++++++
.../camel/language/python/PythonVariablesTest.java | 96 +++++++++++++++
.../src/main/docs/python3-language.adoc | 40 ++++--
.../camel/language/python3/Python3Language.java | 43 ++++---
.../language/python3/Python3BindingNamesTest.java | 126 +++++++++++++++++++
.../language/python3/Python3LanguageEvalTest.java | 2 +-
.../python3/Python3LanguageSecurityTest.java | 4 +-
.../language/python3/Python3VariablesTest.java | 135 +++++++++++++++++++++
.../camel/language/spel/SpelVariablesTest.java | 61 ++++++++++
.../camel-spring/src/main/docs/spel-language.adoc | 16 ++-
.../org/apache/camel/language/spel/RootObject.java | 25 ++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 8 ++
30 files changed, 1206 insertions(+), 84 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
index 7ddc740ab0ad..28f67ca33c24 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/js-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
|=======================================================================
|Variable |Type |Description
|*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
|exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
|exchangeId |String |the exchange id
|message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
|headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught
exception in an error handler), otherwise `null`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
|=======================================================================
+The variables have the same names as in the xref:groovy-language.adoc[Groovy]
language, except `in`, which is a
+reserved word in JavaScript (use `message` or `request`), and the deprecated
`out` and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
== Dependencies
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
index f38cfd68f463..fe52fad1b704 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mvel-language.adoc
@@ -57,13 +57,21 @@ The following Camel related variables are made available:
|request |Message |the message
|message |Message |the message
|headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the
given type
|properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the
given type
|=======================================================================
+`getHeader`, `getProperty` and `getVariable` are methods on the root object,
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`.
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves
global and route
+variables, such as `getVariable('global:foo')`, which are not in the
`variables` map.
+
== Example
For example, you could use MVEL inside a xref:eips:filter-eip.adoc[Message
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
index acd232b4ccd4..5894057a024d 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ognl-language.adoc
@@ -56,13 +56,21 @@ include::partial$language-options.adoc[]
|request |Message |the message
|message |Message |the message
|headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the
given type
|properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the
given type
|=======================================================================
+`getHeader`, `getProperty` and `getVariable` are methods on the root object,
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`.
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves
global and route
+variables, such as `getVariable('global:foo')`, which are not in the
`variables` map.
+
== Example
For example, you could use OGNL inside a xref:eips:filter-eip.adoc[Message
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
index c4d66ef8aa27..486c001e046f 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
|=======================================================================
|Variable |Type |Description
|*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
|exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
|exchangeId |String |the exchange id
|message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
|headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught
exception in an error handler), otherwise `None`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
|=======================================================================
+The variables have the same names as in the xref:groovy-language.adoc[Groovy]
language, except `in`, which is a
+reserved word in Python (use `message` or `request`), and the deprecated `out`
and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
== Dependencies
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
index 68b82eb9bd74..040caedfaef8 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/python3-language.adoc
@@ -32,17 +32,35 @@ The following variables are bound by default:
|=======================================================================
|Variable |Type |Description
|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
|headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
|exchangeId |String |the exchange id
|=======================================================================
-By default, Python can index Java maps and lists (for example `headers['foo']`)
-but cannot invoke methods on host objects.
+The variables have the same names as in the xref:groovy-language.adoc[Groovy]
language. In default mode only the
+data variables above are bound; the Camel host objects are bound only with
trusted host access.
-`message`, `exchange`, and `context` are not bound in default mode. Scripts
that
-refer to them raise a Python `NameError`. Those variables are available only
when
-you opt in to trusted host access, as described in <<_trusted_host_access>>.
+By default, Python can index Java maps and lists (for example `headers['foo']`
+or `variables['foo']`) but cannot invoke methods on host objects.
+
+`variables` holds the exchange-scoped variables (for example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
+such as `global:foo`, are not in this map, so in default mode a script cannot
read
+them; with trusted host access they can be read with
`exchange.getVariable(...)`.
+Like `headers`, it is the exchange's own map rather than a copy, so an
assignment
+such as `variables['foo'] = 'bar'` updates the exchange. Such an assignment
does not
+go through `Exchange.setVariable`, so `variables['global:foo'] = 1` sets an
exchange
+variable named `global:foo`, not a global one. Setting a variable to `None`
fails,
+because the variables map does not accept null values.
+
+`exchange`, `camelContext`, `message`, `request` and `exception` are not
+bound in default mode. Scripts that refer to them raise a Python `NameError`.
Those
+variables are available only when you opt in to trusted host access, as
described in
+<<_trusted_host_access>>.
== Security
@@ -72,8 +90,8 @@ you trust the scripts.
[WARNING]
====
Trusted host access lets Python call public methods on bound Camel objects
such as
-`context`, `exchange`, and `message`. That includes destructive operations, for
-example `context.stop()`, `context.getRegistry().bind(...)`, and
+`camelContext`, `exchange`, and `message`. That includes destructive
operations, for
+example `camelContext.stop()`, `camelContext.getRegistry().bind(...)`, and
`exchange.getContext().getExecutorService(...)`. Enable this mode only for
fully
trusted scripts. Do not use it with Python code taken from untrusted or
external
input.
@@ -84,9 +102,11 @@ In this mode the default variables above remain available,
plus:
[width="100%",cols="10%,10%,80%",options="header",]
|=======================================================================
|Variable |Type |Description
-|message |Message |the message
|exchange |Exchange |the Exchange
-|context |CamelContext |the CamelContext
+|camelContext |CamelContext |the CamelContext
+|message |Message |the message
+|request |Message |the message (same as `message`)
+|exception |Exception |the exception if the exchange failed (or the caught
exception in an error handler), otherwise `None`
|=======================================================================
[source,java]
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
index b1b1e41c577f..76c0c583257f 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spel-language.adoc
@@ -40,13 +40,21 @@ The following Camel related variables are made available:
|request |Message |the message
|message |Message |the message
|headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the
given type
|properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the
given type
|=======================================================================
+`getHeader`, `getProperty` and `getVariable` are methods on the root object,
so they are
+called by their full name, as in `#{getHeader('foo')}` or
`#{getVariable('foo')}`. The maps can be
+read as `#{headers.foo}` or `#{variables['foo']}`. `getVariable` also resolves
global and route
+variables, such as `#{getVariable('global:foo')}`, which are not in the
`variables` map.
+
== Example
You can use SpEL as an expression for
xref:eips:recipientList-eip.adoc[Recipient
diff --git a/components/camel-javascript/src/main/docs/js-language.adoc
b/components/camel-javascript/src/main/docs/js-language.adoc
index 7ddc740ab0ad..28f67ca33c24 100644
--- a/components/camel-javascript/src/main/docs/js-language.adoc
+++ b/components/camel-javascript/src/main/docs/js-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
|=======================================================================
|Variable |Type |Description
|*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
|exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
|exchangeId |String |the exchange id
|message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
|headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught
exception in an error handler), otherwise `null`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
|=======================================================================
+The variables have the same names as in the xref:groovy-language.adoc[Groovy]
language, except `in`, which is a
+reserved word in JavaScript (use `message` or `request`), and the deprecated
`out` and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
== Dependencies
diff --git
a/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
b/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
index 5d360a053574..1c02b7689b30 100644
---
a/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
+++
b/components/camel-javascript/src/main/java/org/apache/camel/language/js/JavaScriptExpression.java
@@ -16,8 +16,12 @@
*/
package org.apache.camel.language.js;
+import java.util.Map;
+
import org.apache.camel.Exchange;
+import org.apache.camel.Message;
import org.apache.camel.support.ExpressionSupport;
+import org.apache.camel.support.LanguageHelper;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
@@ -53,13 +57,28 @@ public class JavaScriptExpression extends ExpressionSupport
{
try (Context cx = lang.newContext()) {
Value b = cx.getBindings("js");
+ Message message = exchange.getMessage();
+ Map<String, Object> headers = message.getHeaders();
+ Map<String, Object> properties = exchange.getAllProperties();
+ Map<String, Object> variables = exchange.getVariables();
+ // the names ExchangeHelper.populateVariableMap gives Groovy,
except in (a reserved word) and the deprecated
+ // out and response
b.putMember("exchange", exchange);
- b.putMember("context", exchange.getContext());
+ b.putMember("camelContext", exchange.getContext());
b.putMember("exchangeId", exchange.getExchangeId());
- b.putMember("message", exchange.getMessage());
- b.putMember("headers", exchange.getMessage().getHeaders());
- b.putMember("properties", exchange.getAllProperties());
- b.putMember("body", exchange.getMessage().getBody());
+ b.putMember("message", message);
+ b.putMember("request", message);
+ b.putMember("body", message.getBody());
+ b.putMember("header", headers);
+ b.putMember("headers", headers);
+ b.putMember("exchangeProperty", properties);
+ b.putMember("exchangeProperties", properties);
+ b.putMember("variable", variables);
+ b.putMember("variables", variables);
+ b.putMember("exception", LanguageHelper.exception(exchange));
+ // deprecated names of camelContext and exchangeProperties
+ b.putMember("context", exchange.getContext());
+ b.putMember("properties", properties);
Value o = cx.eval(lang.source(expressionString));
Object answer = JavaScriptLanguage.materialize(o);
diff --git
a/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptBindingNamesTest.java
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptBindingNamesTest.java
new file mode 100644
index 000000000000..574bfa3bf77d
--- /dev/null
+++
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptBindingNamesTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.language.js;
+
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+/**
+ * The script variables have the names {@code
ExchangeHelper.populateVariableMap} gives Groovy, and the older
+ * {@code context} and {@code properties} still work as deprecated names.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class JavaScriptBindingNamesTest extends LanguageTestSupport {
+
+ @Test
+ void groovyNamesAreBound() {
+ exchange.setProperty("color", "red");
+ exchange.setVariable("foo", "bar");
+
+ assertExpression("exchange.getExchangeId() == exchangeId", true);
+ assertExpression("camelContext.getName()", context.getName());
+ assertExpression("message.getHeader('foo')", "abc");
+ assertExpression("request.getHeader('foo')", "abc");
+ assertExpression("body", "<hello id='m123'>world!</hello>");
+ assertExpression("header.foo", "abc");
+ assertExpression("headers.foo", "abc");
+ assertExpression("exchangeProperty.color", "red");
+ assertExpression("exchangeProperties.color", "red");
+ assertExpression("variable.foo", "bar");
+ assertExpression("variables.foo", "bar");
+ }
+
+ @Test
+ void exceptionIsBound() {
+ assertExpression("exception == null", true);
+
+ exchange.setException(new IllegalArgumentException("Forced"));
+ assertExpression("exception.getMessage()", "Forced");
+ }
+
+ @Test
+ void deprecatedNamesStillWork() {
+ exchange.setProperty("color", "red");
+
+ assertExpression("context.getName()", context.getName());
+ assertExpression("context === camelContext", true);
+ assertExpression("properties.color", "red");
+ }
+
+ @Override
+ protected String getLanguageName() {
+ return "js";
+ }
+}
diff --git
a/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptVariablesTest.java
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptVariablesTest.java
new file mode 100644
index 000000000000..e21065b5e8ca
--- /dev/null
+++
b/components/camel-javascript/src/test/java/org/apache/camel/language/js/JavaScriptVariablesTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.language.js;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exchange variables are bound as {@code variable} and {@code variables},
next to {@code headers} and
+ * {@code properties}.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class JavaScriptVariablesTest extends LanguageTestSupport {
+
+ @Test
+ void variablesAreBound() {
+ exchange.setVariable("foo", "bar");
+ exchange.setVariable("num", 5);
+
+ assertExpression("variables.foo", "bar");
+ assertExpression("variables['foo']", "bar");
+ assertExpression("variable.get('foo')", "bar");
+ assertExpression("variables.num + 2", 7);
+ assertExpression("variables.get('missing')", null);
+ assertPredicate("variables.foo == 'bar'");
+ assertPredicate("variable.foo == 'baz'", false);
+ }
+
+ @Test
+ void variablesAreBoundForIndirectLookup() {
+ exchange.setVariable("foo", "bar");
+
+ assertExpression("globalThis['vari' + 'ables'].foo", "bar");
+ assertExpression("globalThis['vari' + 'able'].foo", "bar");
+ }
+
+ @Test
+ void variablesAreBoundWhenNoVariableIsSet() {
+ Language language = context.resolveLanguage("js");
+ Exchange exchange = new DefaultExchange(context);
+
+ assertThat(language.createExpression("globalThis['vari' +
'ables'].size()").evaluate(exchange, Integer.class))
+ .isZero();
+ }
+
+ @Test
+ void variablesSetInRoute() throws Exception {
+ getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+ getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+ template.sendBodyAndHeader("direct:start", "Camel", "greeting",
"Hello");
+ template.sendBodyAndHeader("direct:start", "World", "greeting",
"Hello");
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .setVariable("name", body())
+ .setVariable("greeting", header("greeting"))
+ .setBody().js("variables.greeting + ' ' +
variable.get('name')")
+ .choice()
+ .when().js("variables.name ==
'Camel'").to("mock:camel")
+ .otherwise().to("mock:other");
+ }
+ };
+ }
+
+ @Override
+ protected String getLanguageName() {
+ return "js";
+ }
+}
diff --git a/components/camel-mvel/src/main/docs/mvel-language.adoc
b/components/camel-mvel/src/main/docs/mvel-language.adoc
index f38cfd68f463..fe52fad1b704 100644
--- a/components/camel-mvel/src/main/docs/mvel-language.adoc
+++ b/components/camel-mvel/src/main/docs/mvel-language.adoc
@@ -57,13 +57,21 @@ The following Camel related variables are made available:
|request |Message |the message
|message |Message |the message
|headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the
given type
|properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the
given type
|=======================================================================
+`getHeader`, `getProperty` and `getVariable` are methods on the root object,
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`.
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves
global and route
+variables, such as `getVariable('global:foo')`, which are not in the
`variables` map.
+
== Example
For example, you could use MVEL inside a xref:eips:filter-eip.adoc[Message
diff --git
a/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
b/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
index bf2fc79bda38..60af18e4020d 100644
---
a/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
+++
b/components/camel-mvel/src/main/java/org/apache/camel/language/mvel/RootObject.java
@@ -81,4 +81,29 @@ public class RootObject {
public <T> T getHeader(String name, Class<T> type) {
return exchange.getMessage().getHeader(name, type);
}
+
+ /**
+ * The variables of the exchange (exchange-scoped only; global and route
variables are not in this map).
+ *
+ * @see #getVariable(String) to look up a global or route variable by its
prefixed name
+ */
+ public Map<String, Object> getVariables() {
+ return exchange.getVariables();
+ }
+
+ /**
+ * The variable with the given name, which can be prefixed with {@code
global:} or {@code route:} for a global or
+ * route variable.
+ */
+ public Object getVariable(String name) {
+ return exchange.getVariable(name);
+ }
+
+ /**
+ * The variable with the given name converted to the given type, which can
be prefixed with {@code global:} or
+ * {@code route:} for a global or route variable.
+ */
+ public <T> T getVariable(String name, Class<T> type) {
+ return exchange.getVariable(name, type);
+ }
}
diff --git
a/components/camel-mvel/src/test/java/org/apache/camel/language/mvel/MvelVariablesTest.java
b/components/camel-mvel/src/test/java/org/apache/camel/language/mvel/MvelVariablesTest.java
new file mode 100644
index 000000000000..60dbd517ab96
--- /dev/null
+++
b/components/camel-mvel/src/test/java/org/apache/camel/language/mvel/MvelVariablesTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.language.mvel;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exchange variables are exposed on the root object as {@code variables},
{@code getVariable(name)} and
+ * {@code getVariable(name, type)}, mirroring {@code headers} and {@code
getHeader(...)}.
+ */
+class MvelVariablesTest extends LanguageTestSupport {
+
+ @Test
+ void variablesAreBound() {
+ exchange.setVariable("foo", "bar");
+ exchange.setVariable("num", 5);
+
+ assertExpression("variables.foo", "bar");
+ assertExpression("variables['foo']", "bar");
+ assertExpression("getVariable('foo')", "bar");
+ assertExpression("variables.num + 2", 7);
+ assertExpression("getVariable('missing')", null);
+ assertPredicate("variables.foo == 'bar'");
+ assertPredicate("getVariable('foo') == 'baz'", false);
+ }
+
+ @Test
+ void typedVariableGetterConverts() {
+ exchange.setVariable("num", "123");
+
+ assertExpression("getVariable('num', Integer) + 1", 124);
+ assertPredicate("getVariable('num', Integer) > 100");
+ }
+
+ @Test
+ void globalVariableThroughGetter() {
+ exchange.setVariable("global:greeting", "Hi");
+
+ assertExpression("getVariable('global:greeting')", "Hi");
+ }
+
+ @Test
+ void variablesSetInRoute() throws Exception {
+ getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+ getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+ template.sendBodyAndHeader("direct:start", "Camel", "greeting",
"Hello");
+ template.sendBodyAndHeader("direct:start", "World", "greeting",
"Hello");
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .setVariable("name", body())
+ .setVariable("greeting", header("greeting"))
+ .setBody().mvel("variables.greeting + ' ' +
getVariable('name')")
+ .choice()
+ .when().mvel("variables.name ==
'Camel'").to("mock:camel")
+ .otherwise().to("mock:other");
+ }
+ };
+ }
+
+ @Override
+ protected String getLanguageName() {
+ return "mvel";
+ }
+}
diff --git a/components/camel-ognl/src/main/docs/ognl-language.adoc
b/components/camel-ognl/src/main/docs/ognl-language.adoc
index acd232b4ccd4..5894057a024d 100644
--- a/components/camel-ognl/src/main/docs/ognl-language.adoc
+++ b/components/camel-ognl/src/main/docs/ognl-language.adoc
@@ -56,13 +56,21 @@ include::partial$language-options.adoc[]
|request |Message |the message
|message |Message |the message
|headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the
given type
|properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the
given type
|=======================================================================
+`getHeader`, `getProperty` and `getVariable` are methods on the root object,
so they are
+called by their full name, as in `getHeader('foo')` or `getVariable('foo')`.
The maps can be
+read as `headers.foo` or `variables['foo']`. `getVariable` also resolves
global and route
+variables, such as `getVariable('global:foo')`, which are not in the
`variables` map.
+
== Example
For example, you could use OGNL inside a xref:eips:filter-eip.adoc[Message
diff --git
a/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
b/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
index f1ba1722330d..d323912cfa0a 100644
---
a/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
+++
b/components/camel-ognl/src/main/java/org/apache/camel/language/ognl/RootObject.java
@@ -78,4 +78,29 @@ public class RootObject {
return exchange.getMessage().getHeader(name, type);
}
+ /**
+ * The variables of the exchange (exchange-scoped only; global and route
variables are not in this map).
+ *
+ * @see #getVariable(String) to look up a global or route variable by its
prefixed name
+ */
+ public Map<String, Object> getVariables() {
+ return exchange.getVariables();
+ }
+
+ /**
+ * The variable with the given name, which can be prefixed with {@code
global:} or {@code route:} for a global or
+ * route variable.
+ */
+ public Object getVariable(String name) {
+ return exchange.getVariable(name);
+ }
+
+ /**
+ * The variable with the given name converted to the given type, which can
be prefixed with {@code global:} or
+ * {@code route:} for a global or route variable.
+ */
+ public <T> T getVariable(String name, Class<T> type) {
+ return exchange.getVariable(name, type);
+ }
+
}
diff --git
a/components/camel-ognl/src/test/java/org/apache/camel/language/ognl/OgnlVariablesTest.java
b/components/camel-ognl/src/test/java/org/apache/camel/language/ognl/OgnlVariablesTest.java
new file mode 100644
index 000000000000..af2dbaad2529
--- /dev/null
+++
b/components/camel-ognl/src/test/java/org/apache/camel/language/ognl/OgnlVariablesTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.language.ognl;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exchange variables are exposed on the root object as {@code variables},
{@code getVariable(name)} and
+ * {@code getVariable(name, type)}, mirroring {@code headers} and {@code
getHeader(...)}.
+ */
+class OgnlVariablesTest extends LanguageTestSupport {
+
+ @Test
+ void variablesAreBound() {
+ exchange.setVariable("foo", "bar");
+ exchange.setVariable("num", 5);
+
+ assertExpression("variables.foo", "bar");
+ assertExpression("variables['foo']", "bar");
+ assertExpression("getVariable('foo')", "bar");
+ assertExpression("variables.num + 2", 7);
+ assertExpression("getVariable('missing')", null);
+ assertPredicate("variables.foo == 'bar'");
+ assertPredicate("getVariable('foo') == 'baz'", false);
+ }
+
+ @Test
+ void typedVariableGetterConverts() {
+ exchange.setVariable("num", "123");
+
+ assertExpression("getVariable('num', @java.lang.Integer@class) + 1",
124);
+ assertPredicate("getVariable('num', @java.lang.Integer@class) > 100");
+ }
+
+ @Test
+ void globalVariableThroughGetter() {
+ exchange.setVariable("global:greeting", "Hi");
+
+ assertExpression("getVariable('global:greeting')", "Hi");
+ }
+
+ @Test
+ void variablesSetInRoute() throws Exception {
+ getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+ getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+ template.sendBodyAndHeader("direct:start", "Camel", "greeting",
"Hello");
+ template.sendBodyAndHeader("direct:start", "World", "greeting",
"Hello");
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .setVariable("name", body())
+ .setVariable("greeting", header("greeting"))
+ .setBody().ognl("variables.greeting + ' ' +
getVariable('name')")
+ .choice()
+ .when().ognl("variables.name ==
'Camel'").to("mock:camel")
+ .otherwise().to("mock:other");
+ }
+ };
+ }
+
+ @Override
+ protected String getLanguageName() {
+ return "ognl";
+ }
+}
diff --git a/components/camel-python/src/main/docs/python-language.adoc
b/components/camel-python/src/main/docs/python-language.adoc
index c4d66ef8aa27..486c001e046f 100644
--- a/components/camel-python/src/main/docs/python-language.adoc
+++ b/components/camel-python/src/main/docs/python-language.adoc
@@ -30,15 +30,31 @@ include::partial$language-options.adoc[]
|=======================================================================
|Variable |Type |Description
|*this* |Exchange |the Exchange is the root object
-|context |CamelContext |the CamelContext
|exchange |Exchange |the Exchange
+|camelContext |CamelContext |the CamelContext
|exchangeId |String |the exchange id
|message |Message |the message
-|body |Message |the message body
+|request |Message |the message (same as `message`)
+|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
|headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
+|exception |Exception |the exception if the exchange failed (or the caught
exception in an error handler), otherwise `None`
+|context |CamelContext |*Deprecated* use `camelContext`
+|properties |Map |*Deprecated* use `exchangeProperties`
|=======================================================================
+The variables have the same names as in the xref:groovy-language.adoc[Groovy]
language, except `in`, which is a
+reserved word in Python (use `message` or `request`), and the deprecated `out`
and `response`. The deprecated
+`context` and `properties` still work.
+
+The `variable` and `variables` bindings are the exchange-scoped variables (for
example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
such as `global:foo`,
+are not in this map; use `exchange.getVariable('global:foo')` for those.
+
== Dependencies
diff --git
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
index 3de078780711..af1ab5077744 100644
---
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
+++
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
@@ -16,9 +16,13 @@
*/
package org.apache.camel.language.python;
+import java.util.Map;
+
import org.apache.camel.Exchange;
import org.apache.camel.ExpressionIllegalSyntaxException;
+import org.apache.camel.Message;
import org.apache.camel.support.ExpressionSupport;
+import org.apache.camel.support.LanguageHelper;
import org.python.core.PyCode;
import org.python.core.PyObject;
import org.python.util.PythonInterpreter;
@@ -57,13 +61,28 @@ public class PythonExpression extends ExpressionSupport {
private <T> T doEvaluate(Exchange exchange, Class<T> type) {
try {
+ Message message = exchange.getMessage();
+ Map<String, Object> headers = message.getHeaders();
+ Map<String, Object> properties = exchange.getAllProperties();
+ Map<String, Object> variables = exchange.getVariables();
+ // the names ExchangeHelper.populateVariableMap gives Groovy,
except in (a reserved word) and the deprecated
+ // out and response
compiler.set("exchange", exchange);
- compiler.set("context", exchange.getContext());
+ compiler.set("camelContext", exchange.getContext());
compiler.set("exchangeId", exchange.getExchangeId());
- compiler.set("message", exchange.getMessage());
- compiler.set("headers", exchange.getMessage().getHeaders());
- compiler.set("properties", exchange.getAllProperties());
- compiler.set("body", exchange.getMessage().getBody());
+ compiler.set("message", message);
+ compiler.set("request", message);
+ compiler.set("body", message.getBody());
+ compiler.set("header", headers);
+ compiler.set("headers", headers);
+ compiler.set("exchangeProperty", properties);
+ compiler.set("exchangeProperties", properties);
+ compiler.set("variable", variables);
+ compiler.set("variables", variables);
+ compiler.set("exception", LanguageHelper.exception(exchange));
+ // deprecated names of camelContext and exchangeProperties
+ compiler.set("context", exchange.getContext());
+ compiler.set("properties", properties);
PyObject out = compiler.eval(compiledExpression);
if (out != null) {
diff --git
a/components/camel-python/src/test/java/org/apache/camel/language/python/PythonBindingNamesTest.java
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonBindingNamesTest.java
new file mode 100644
index 000000000000..776e443b68de
--- /dev/null
+++
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonBindingNamesTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.language.python;
+
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The script variables have the names {@code
ExchangeHelper.populateVariableMap} gives Groovy, and the older
+ * {@code context} and {@code properties} still work as deprecated names.
+ */
+class PythonBindingNamesTest extends LanguageTestSupport {
+
+ @Test
+ void groovyNamesAreBound() {
+ exchange.setProperty("color", "red");
+ exchange.setVariable("foo", "bar");
+
+ assertExpression("exchange.getExchangeId() == exchangeId", true);
+ assertExpression("camelContext.getName()", context.getName());
+ assertExpression("message.getHeader('foo')", "abc");
+ assertExpression("request.getHeader('foo')", "abc");
+ assertExpression("body", "<hello id='m123'>world!</hello>");
+ assertExpression("header['foo']", "abc");
+ assertExpression("headers['foo']", "abc");
+ assertExpression("exchangeProperty['color']", "red");
+ assertExpression("exchangeProperties['color']", "red");
+ assertExpression("variable['foo']", "bar");
+ assertExpression("variables['foo']", "bar");
+ }
+
+ @Test
+ void exceptionIsBound() {
+ assertExpression("exception is None", true);
+
+ exchange.setException(new IllegalArgumentException("Forced"));
+ assertExpression("exception.getMessage()", "Forced");
+ }
+
+ @Test
+ void deprecatedNamesStillWork() {
+ exchange.setProperty("color", "red");
+
+ assertExpression("context.getName()", context.getName());
+ assertExpression("context == camelContext", true);
+ assertExpression("properties['color']", "red");
+ }
+
+ @Override
+ protected String getLanguageName() {
+ return "python";
+ }
+}
diff --git
a/components/camel-python/src/test/java/org/apache/camel/language/python/PythonVariablesTest.java
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonVariablesTest.java
new file mode 100644
index 000000000000..29b9d8e928cd
--- /dev/null
+++
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonVariablesTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.language.python;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Exchange variables are bound as {@code variable} and {@code variables},
next to {@code headers} and
+ * {@code properties}.
+ */
+class PythonVariablesTest extends LanguageTestSupport {
+
+ @Test
+ void variablesAreBound() {
+ exchange.setVariable("foo", "bar");
+ exchange.setVariable("num", 5);
+
+ assertExpression("variables['foo']", "bar");
+ assertExpression("variable['foo']", "bar");
+ assertExpression("variables['num'] + 2", 7);
+ assertExpression("variables.get('missing') is None", true);
+ assertPredicate("variables['foo'] == 'bar'");
+ assertPredicate("variables['foo'] == 'baz'", false);
+ }
+
+ @Test
+ void variablesAreBoundForIndirectLookup() {
+ exchange.setVariable("foo", "bar");
+
+ assertExpression("globals()['vari' + 'ables']['foo']", "bar");
+ assertExpression("globals()['vari' + 'able']['foo']", "bar");
+ }
+
+ @Test
+ void variablesAreBoundWhenNoVariableIsSet() {
+ Language language = context.resolveLanguage("python");
+ Exchange exchange = new DefaultExchange(context);
+
+ assertEquals("0", language.createExpression("str(len(globals()['vari'
+ 'ables']))").evaluate(exchange, String.class));
+ }
+
+ @Test
+ void variablesSetInRoute() throws Exception {
+ getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+ getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+ template.sendBodyAndHeader("direct:start", "Camel", "greeting",
"Hello");
+ template.sendBodyAndHeader("direct:start", "World", "greeting",
"Hello");
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .setVariable("name", body())
+ .setVariable("greeting", header("greeting"))
+ .setBody().python("variables['greeting'] + ' ' +
variable['name']")
+ .choice()
+ .when().python("variables['name'] ==
'Camel'").to("mock:camel")
+ .otherwise().to("mock:other");
+ }
+ };
+ }
+
+ @Override
+ protected String getLanguageName() {
+ return "python";
+ }
+}
diff --git a/components/camel-python3/src/main/docs/python3-language.adoc
b/components/camel-python3/src/main/docs/python3-language.adoc
index 68b82eb9bd74..040caedfaef8 100644
--- a/components/camel-python3/src/main/docs/python3-language.adoc
+++ b/components/camel-python3/src/main/docs/python3-language.adoc
@@ -32,17 +32,35 @@ The following variables are bound by default:
|=======================================================================
|Variable |Type |Description
|body |Object |the message body
+|header |Map |the message headers (same as `headers`)
|headers |Map |the message headers
-|properties |Map |the exchange properties
+|exchangeProperty |Map |the exchange properties (same as `exchangeProperties`)
+|exchangeProperties |Map |the exchange properties
+|variable |Map |the exchange variables (same as `variables`)
+|variables |Map |the exchange variables
|exchangeId |String |the exchange id
|=======================================================================
-By default, Python can index Java maps and lists (for example `headers['foo']`)
-but cannot invoke methods on host objects.
+The variables have the same names as in the xref:groovy-language.adoc[Groovy]
language. In default mode only the
+data variables above are bound; the Camel host objects are bound only with
trusted host access.
-`message`, `exchange`, and `context` are not bound in default mode. Scripts
that
-refer to them raise a Python `NameError`. Those variables are available only
when
-you opt in to trusted host access, as described in <<_trusted_host_access>>.
+By default, Python can index Java maps and lists (for example `headers['foo']`
+or `variables['foo']`) but cannot invoke methods on host objects.
+
+`variables` holds the exchange-scoped variables (for example set with the
+xref:eips:setVariable-eip.adoc[Set Variable] EIP). Global and route variables,
+such as `global:foo`, are not in this map, so in default mode a script cannot
read
+them; with trusted host access they can be read with
`exchange.getVariable(...)`.
+Like `headers`, it is the exchange's own map rather than a copy, so an
assignment
+such as `variables['foo'] = 'bar'` updates the exchange. Such an assignment
does not
+go through `Exchange.setVariable`, so `variables['global:foo'] = 1` sets an
exchange
+variable named `global:foo`, not a global one. Setting a variable to `None`
fails,
+because the variables map does not accept null values.
+
+`exchange`, `camelContext`, `message`, `request` and `exception` are not
+bound in default mode. Scripts that refer to them raise a Python `NameError`.
Those
+variables are available only when you opt in to trusted host access, as
described in
+<<_trusted_host_access>>.
== Security
@@ -72,8 +90,8 @@ you trust the scripts.
[WARNING]
====
Trusted host access lets Python call public methods on bound Camel objects
such as
-`context`, `exchange`, and `message`. That includes destructive operations, for
-example `context.stop()`, `context.getRegistry().bind(...)`, and
+`camelContext`, `exchange`, and `message`. That includes destructive
operations, for
+example `camelContext.stop()`, `camelContext.getRegistry().bind(...)`, and
`exchange.getContext().getExecutorService(...)`. Enable this mode only for
fully
trusted scripts. Do not use it with Python code taken from untrusted or
external
input.
@@ -84,9 +102,11 @@ In this mode the default variables above remain available,
plus:
[width="100%",cols="10%,10%,80%",options="header",]
|=======================================================================
|Variable |Type |Description
-|message |Message |the message
|exchange |Exchange |the Exchange
-|context |CamelContext |the CamelContext
+|camelContext |CamelContext |the CamelContext
+|message |Message |the message
+|request |Message |the message (same as `message`)
+|exception |Exception |the exception if the exchange failed (or the caught
exception in an error handler), otherwise `None`
|=======================================================================
[source,java]
diff --git
a/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
b/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
index 695dd08d9582..f8f15d99c31b 100644
---
a/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
+++
b/components/camel-python3/src/main/java/org/apache/camel/language/python3/Python3Language.java
@@ -32,12 +32,14 @@ import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.ExpressionEvaluationException;
import org.apache.camel.ExpressionIllegalSyntaxException;
+import org.apache.camel.Message;
import org.apache.camel.Predicate;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.Service;
import org.apache.camel.spi.ScriptingLanguage;
import org.apache.camel.spi.annotations.Language;
import org.apache.camel.support.LRUCacheFactory;
+import org.apache.camel.support.LanguageHelper;
import org.apache.camel.support.TypedLanguageSupport;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
@@ -50,10 +52,11 @@ import org.graalvm.polyglot.Value;
* Camel expression language for Python 3 via <a
href="https://www.graalvm.org/python/">GraalPy</a>.
*
* <p>
- * Default scripts see only data bindings: {@code body}, {@code headers},
{@code properties}, and {@code exchangeId}.
- * {@code exchange}, {@code message}, and {@code context} are intentionally
absent so they resolve as Python
- * {@code NameError} rather than opaque objects with no usable API. Binding
them by default would also become a
- * privilege escalation if host access were later widened.
+ * Default scripts see only data bindings: {@code body}, {@code header}/{@code
headers},
+ * {@code exchangeProperty}/{@code exchangeProperties}, {@code
variable}/{@code variables} and {@code exchangeId}. The
+ * host objects ({@code exchange}, {@code camelContext}, {@code message},
{@code request} and {@code exception}) are
+ * intentionally absent so they resolve as Python {@code NameError} rather
than opaque objects with no usable API.
+ * Binding them by default would also become a privilege escalation if host
access were later widened.
* </p>
*
* <p>
@@ -72,8 +75,8 @@ public class Python3Language extends TypedLanguageSupport
implements ScriptingLa
private final HostAccess hostAccess;
/**
- * When true, also bind {@code exchange}, {@code message}, and {@code
context}. Only {@link #createWithHostAccess()}
- * sets this; default mode keeps those names undefined.
+ * When true, also bind the host objects {@code exchange}, {@code
camelContext}, {@code message}, {@code request}
+ * and {@code exception}. Only {@link #createWithHostAccess()} sets this;
default mode keeps those names undefined.
*/
private final boolean bindCamelHostObjects;
private final Map<String, Source> sourceCache =
LRUCacheFactory.newLRUSoftCache(16, 1000, true);
@@ -95,8 +98,8 @@ public class Python3Language extends TypedLanguageSupport
implements ScriptingLa
/**
* Creates a separate language instance for trusted scripts. Uses {@link
HostAccess#ALL} so Python may call public
- * methods and fields on bound host objects, and additionally exposes
{@code exchange}, {@code message}, and
- * {@code context}.
+ * methods and fields on bound host objects, and additionally exposes
{@code exchange}, {@code camelContext},
+ * {@code message}, {@code request} and {@code exception}.
* <p>
* This is an explicit opt-in: {@code HostAccess.ALL} is not a sandbox. It
does not enable {@code allowAllAccess},
* Java class lookup, host IO, or process creation. Use only when you
trust the scripts.
@@ -166,16 +169,26 @@ public class Python3Language extends TypedLanguageSupport
implements ScriptingLa
Object evaluateExpression(String script, Exchange exchange) {
try (Context cx = Python3Helper.newContext(engine(), hostAccess)) {
Value b = cx.getBindings("python");
- // Default: data only. Do not bind exchange/message/context — they
are undefined (NameError)
- // unless createWithHostAccess() opted into trusted host-object
bindings.
+ Message message = exchange.getMessage();
+ Map<String, Object> headers = message.getHeaders();
+ Map<String, Object> properties = exchange.getAllProperties();
+ Map<String, Object> variables = exchange.getVariables();
+ // Default: data only, with the data names
ExchangeHelper.populateVariableMap gives Groovy. The host objects
+ // are undefined (NameError) unless createWithHostAccess() opted
into trusted host-object bindings.
b.putMember("exchangeId", exchange.getExchangeId());
- b.putMember("headers", exchange.getMessage().getHeaders());
- b.putMember("properties", exchange.getAllProperties());
- b.putMember("body", exchange.getMessage().getBody());
+ b.putMember("body", message.getBody());
+ b.putMember("header", headers);
+ b.putMember("headers", headers);
+ b.putMember("exchangeProperty", properties);
+ b.putMember("exchangeProperties", properties);
+ b.putMember("variable", variables);
+ b.putMember("variables", variables);
if (bindCamelHostObjects) {
b.putMember("exchange", exchange);
- b.putMember("message", exchange.getMessage());
- b.putMember("context", exchange.getContext());
+ b.putMember("camelContext", exchange.getContext());
+ b.putMember("message", message);
+ b.putMember("request", message);
+ b.putMember("exception", LanguageHelper.exception(exchange));
}
Value value = cx.eval(source(script));
return convert(value, Object.class, exchange.getContext(),
exchange);
diff --git
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3BindingNamesTest.java
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3BindingNamesTest.java
new file mode 100644
index 000000000000..16450b322b27
--- /dev/null
+++
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3BindingNamesTest.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.language.python3;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static
org.apache.camel.language.python3.Python3LanguageSecurityTest.assertNameError;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The script variables have the names {@code
ExchangeHelper.populateVariableMap} gives Groovy: default mode binds only
+ * the data names, trusted host access also the host objects. python3 is new,
so the deprecated {@code properties} and
+ * {@code context} of the js and python languages are not bound in either mode.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class Python3BindingNamesTest extends CamelTestSupport {
+
+ private Exchange sampleExchange() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setBody("hello");
+ exchange.getIn().setHeader("foo", "abc");
+ exchange.setProperty("color", "red");
+ exchange.setVariable("name", "Camel");
+ return exchange;
+ }
+
+ @Test
+ void defaultModeBindsGroovyDataNames() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = sampleExchange();
+
+ assertThat(evaluate(language, exchange, "body")).isEqualTo("hello");
+ assertThat(evaluate(language, exchange,
"header['foo']")).isEqualTo("abc");
+ assertThat(evaluate(language, exchange,
"headers['foo']")).isEqualTo("abc");
+ assertThat(evaluate(language, exchange,
"exchangeProperty['color']")).isEqualTo("red");
+ assertThat(evaluate(language, exchange,
"exchangeProperties['color']")).isEqualTo("red");
+ assertThat(evaluate(language, exchange,
"variable['name']")).isEqualTo("Camel");
+ assertThat(evaluate(language, exchange,
"variables['name']")).isEqualTo("Camel");
+ assertThat(evaluate(language, exchange,
"exchangeId")).isEqualTo(exchange.getExchangeId());
+ }
+
+ @Test
+ void defaultModeDoesNotBindDeprecatedNames() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = sampleExchange();
+
+ assertNameError(language, exchange, "properties");
+ assertNameError(language, exchange, "context");
+ }
+
+ @Test
+ void defaultModeDoesNotBindHostObjects() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = sampleExchange();
+ exchange.setException(new IllegalArgumentException("Forced"));
+
+ assertNameError(language, exchange, "exchange");
+ assertNameError(language, exchange, "camelContext");
+ assertNameError(language, exchange, "message");
+ assertNameError(language, exchange, "request");
+ assertNameError(language, exchange, "exception");
+ assertNameError(language, exchange, "context");
+ }
+
+ @Test
+ void trustedModeBindsGroovyNames() {
+ Python3Language trusted = Python3Language.createWithHostAccess();
+ trusted.setCamelContext(context);
+ trusted.start();
+ try {
+ Exchange exchange = sampleExchange();
+
+ assertThat(evaluate(trusted, exchange,
"exchange.getExchangeId()")).isEqualTo(exchange.getExchangeId());
+ assertThat(evaluate(trusted, exchange,
"camelContext.getName()")).isEqualTo(context.getName());
+ assertThat(evaluate(trusted, exchange,
"message.getHeader('foo')")).isEqualTo("abc");
+ assertThat(evaluate(trusted, exchange,
"request.getHeader('foo')")).isEqualTo("abc");
+ assertThat(evaluate(trusted, exchange,
"header['foo']")).isEqualTo("abc");
+ assertThat(evaluate(trusted, exchange,
"exchangeProperties['color']")).isEqualTo("red");
+ assertThat(evaluate(trusted, exchange,
"variables['name']")).isEqualTo("Camel");
+ assertThat(trusted.createPredicate("exception is
None").matches(exchange)).isTrue();
+
+ exchange.setException(new IllegalArgumentException("Forced"));
+ assertThat(evaluate(trusted, exchange,
"exception.getMessage()")).isEqualTo("Forced");
+ } finally {
+ trusted.stop();
+ }
+ }
+
+ @Test
+ void trustedModeDoesNotBindDeprecatedNames() {
+ Python3Language trusted = Python3Language.createWithHostAccess();
+ trusted.setCamelContext(context);
+ trusted.start();
+ try {
+ Exchange exchange = sampleExchange();
+
+ assertNameError(trusted, exchange, "context");
+ assertNameError(trusted, exchange, "properties");
+ } finally {
+ trusted.stop();
+ }
+ }
+
+ private static String evaluate(Language language, Exchange exchange,
String script) {
+ return language.createExpression(script).evaluate(exchange,
String.class);
+ }
+}
diff --git
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
index 5d39717e2f09..1448fe9c0b12 100644
---
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
+++
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageEvalTest.java
@@ -92,7 +92,7 @@ class Python3LanguageEvalTest {
Language language = context.resolveLanguage("python3");
assertThat(language.createExpression("2 + body").evaluate(exchange,
Integer.class)).isEqualTo(9);
assertThat(language.createExpression("headers['foo']").evaluate(exchange,
String.class)).isEqualTo("bar");
-
assertThat(language.createExpression("properties['color']").evaluate(exchange,
String.class)).isEqualTo("red");
+
assertThat(language.createExpression("exchangeProperties['color']").evaluate(exchange,
String.class)).isEqualTo("red");
assertThat(language.createExpression("exchangeId").evaluate(exchange,
String.class))
.isEqualTo(exchange.getExchangeId());
assertThat(language.createPredicate("body ==
7").matches(exchange)).isTrue();
diff --git
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
index 1c6550c6134e..1bac95904a7e 100644
---
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
+++
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3LanguageSecurityTest.java
@@ -76,7 +76,7 @@ class Python3LanguageSecurityTest {
assertThat(language.createExpression("headers['written'] =
'yes'\nheaders['written']").evaluate(exchange,
String.class)).isEqualTo("yes");
assertThat(exchange.getIn().getHeader("written")).isEqualTo("yes");
-
assertThat(language.createExpression("properties['color']").evaluate(exchange,
String.class)).isEqualTo("red");
+
assertThat(language.createExpression("exchangeProperties['color']").evaluate(exchange,
String.class)).isEqualTo("red");
assertThat(language.createExpression("exchangeId").evaluate(exchange,
String.class))
.isEqualTo(exchange.getExchangeId());
}
@@ -121,7 +121,7 @@ class Python3LanguageSecurityTest {
.isEqualTo(exchange.getExchangeId());
assertThat(trusted.createExpression("message.getBody()").evaluate(exchange,
Object.class))
.isInstanceOf(Person.class);
-
assertThat(trusted.createExpression("context.getName()").evaluate(exchange,
String.class))
+
assertThat(trusted.createExpression("camelContext.getName()").evaluate(exchange,
String.class))
.isEqualTo(context.getName());
assertThat(trusted.createExpression("body.getAge()").evaluate(exchange,
Integer.class)).isEqualTo(36);
assertThat(trusted.createExpression("body.name").evaluate(exchange,
String.class)).isEqualTo("Ada");
diff --git
a/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3VariablesTest.java
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3VariablesTest.java
new file mode 100644
index 000000000000..a7f77dfd1ced
--- /dev/null
+++
b/components/camel-python3/src/test/java/org/apache/camel/language/python3/Python3VariablesTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.language.python3;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static
org.apache.camel.language.python3.Python3LanguageSecurityTest.assertDenied;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exchange variables are bound as the data binding {@code variables}, in the
same way as {@code headers}: default mode
+ * may index the map but may not call Java methods on it.
+ */
+@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)(s390x|ppc64le)")
+class Python3VariablesTest extends CamelTestSupport {
+
+ @Test
+ void defaultModeBindsVariablesAsData() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = new DefaultExchange(context);
+ exchange.setVariable("foo", "bar");
+ exchange.setVariable("num", 5);
+
+
assertThat(language.createExpression("variables['foo']").evaluate(exchange,
String.class)).isEqualTo("bar");
+ assertThat(language.createExpression("variables['num'] +
2").evaluate(exchange, Integer.class)).isEqualTo(7);
+ assertThat(language.createExpression("'missing' in
variables").evaluate(exchange, Boolean.class)).isFalse();
+ assertThat(language.createPredicate("variables['foo'] ==
'bar'").matches(exchange)).isTrue();
+ assertThat(language.createPredicate("variables['foo'] ==
'baz'").matches(exchange)).isFalse();
+ }
+
+ @Test
+ void defaultModeWritesPropagateLikeHeaders() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = new DefaultExchange(context);
+ exchange.setVariable("foo", "bar");
+
+ assertThat(language.createExpression("variables['written'] =
'yes'\nvariables['written']")
+ .evaluate(exchange, String.class)).isEqualTo("yes");
+ assertThat(exchange.getVariable("written")).isEqualTo("yes");
+ }
+
+ @Test
+ void defaultModeDeniesJavaMethodsOnVariables() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = new DefaultExchange(context);
+ exchange.setVariable("foo", "bar");
+
+ assertDenied(language, exchange, "variables.put('k', 'v')",
"AttributeError");
+ assertDenied(language, exchange, "variables.getClass()",
"AttributeError");
+ }
+
+ @Test
+ void trustedModeAlsoBindsVariables() {
+ Python3Language trusted = Python3Language.createWithHostAccess();
+ trusted.setCamelContext(context);
+ trusted.start();
+ try {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.setVariable("foo", "bar");
+
+
assertThat(trusted.createExpression("variables['foo']").evaluate(exchange,
String.class)).isEqualTo("bar");
+
assertThat(trusted.createExpression("exchange.getVariable('foo')").evaluate(exchange,
String.class))
+ .isEqualTo("bar");
+ } finally {
+ trusted.stop();
+ }
+ }
+
+ @Test
+ void variablesAreBoundForIndirectLookup() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = new DefaultExchange(context);
+ exchange.setVariable("foo", "bar");
+
+ assertThat(language.createExpression("globals()['vari' +
'ables']['foo']").evaluate(exchange, String.class))
+ .isEqualTo("bar");
+ }
+
+ @Test
+ void variablesAreBoundWhenNoVariableIsSet() {
+ Language language = context.resolveLanguage("python3");
+ Exchange exchange = new DefaultExchange(context);
+
+ assertThat(language.createPredicate("'foo' in globals()['vari' +
'ables']").matches(exchange)).isFalse();
+ }
+
+ @Test
+ void variablesSetInRoute() throws Exception {
+ getMockEndpoint("mock:camel").expectedBodiesReceived("Hello Camel");
+ getMockEndpoint("mock:other").expectedBodiesReceived("Hello World");
+
+ template.sendBodyAndHeader("direct:start", "Camel", "greeting",
"Hello");
+ template.sendBodyAndHeader("direct:start", "World", "greeting",
"Hello");
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .setVariable("name", body())
+ .setVariable("greeting", header("greeting"))
+ .setBody().python3("f\"{variables['greeting']}
{variables['name']}\"")
+ .choice()
+ .when().python3("variables['name'] ==
'Camel'").to("mock:camel")
+ .otherwise().to("mock:other");
+ }
+ };
+ }
+}
diff --git
a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/language/spel/SpelVariablesTest.java
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/language/spel/SpelVariablesTest.java
new file mode 100644
index 000000000000..2a5629ea914f
--- /dev/null
+++
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/language/spel/SpelVariablesTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.language.spel;
+
+import org.apache.camel.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exchange variables are exposed on the root object as {@code variables},
{@code getVariable(name)} and
+ * {@code getVariable(name, type)}, mirroring {@code headers} and {@code
getHeader(...)}.
+ */
+class SpelVariablesTest extends LanguageTestSupport {
+
+ @Test
+ void variablesAreBound() {
+ exchange.setVariable("foo", "bar");
+ exchange.setVariable("num", 5);
+
+ assertExpression("#{variables.foo}", "bar");
+ assertExpression("#{variables['foo']}", "bar");
+ assertExpression("#{getVariable('foo')}", "bar");
+ assertExpression("#{variables.num + 2}", 7);
+ assertExpression("#{getVariable('missing')}", null);
+ assertPredicate("#{variables.foo == 'bar'}");
+ assertPredicate("#{getVariable('foo') == 'baz'}", false);
+ }
+
+ @Test
+ void typedVariableGetterConverts() {
+ exchange.setVariable("num", "123");
+
+ assertExpression("#{getVariable('num', T(Integer)) + 1}", 124);
+ assertPredicate("#{getVariable('num', T(Integer)) > 100}");
+ }
+
+ @Test
+ void globalVariableThroughGetter() {
+ exchange.setVariable("global:greeting", "Hi");
+
+ assertExpression("#{getVariable('global:greeting')}", "Hi");
+ }
+
+ @Override
+ protected String getLanguageName() {
+ return "spel";
+ }
+}
diff --git
a/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc
b/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc
index b1b1e41c577f..76c0c583257f 100644
---
a/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc
+++
b/components/camel-spring-parent/camel-spring/src/main/docs/spel-language.adoc
@@ -40,13 +40,21 @@ The following Camel related variables are made available:
|request |Message |the message
|message |Message |the message
|headers |Map |the message headers
-|header(name) |Object |the message header by the given name
-|header(name, type) |Type |the message header by the given name as the given
type
+|getHeader(name) |Object |the message header by the given name
+|getHeader(name, type) |Type |the message header by the given name as the
given type
|properties |Map |the exchange properties
-|property(name) |Object |the exchange property by the given name
-|property(name, type) |Type |the exchange property by the given name as the
given type
+|getProperty(name) |Object |the exchange property by the given name
+|getProperty(name, type) |Type |the exchange property by the given name as the
given type
+|variables |Map |the exchange variables
+|getVariable(name) |Object |the exchange variable by the given name
+|getVariable(name, type) |Type |the exchange variable by the given name as the
given type
|=======================================================================
+`getHeader`, `getProperty` and `getVariable` are methods on the root object,
so they are
+called by their full name, as in `#{getHeader('foo')}` or
`#{getVariable('foo')}`. The maps can be
+read as `#{headers.foo}` or `#{variables['foo']}`. `getVariable` also resolves
global and route
+variables, such as `#{getVariable('global:foo')}`, which are not in the
`variables` map.
+
== Example
You can use SpEL as an expression for
xref:eips:recipientList-eip.adoc[Recipient
diff --git
a/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
b/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
index 8f6c4ce7296e..1bb18d8e9bff 100644
---
a/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
+++
b/components/camel-spring-parent/camel-spring/src/main/java/org/apache/camel/language/spel/RootObject.java
@@ -86,4 +86,29 @@ public final class RootObject {
return exchange.getMessage().getHeader(name, type);
}
+ /**
+ * The variables of the exchange (exchange-scoped only; global and route
variables are not in this map).
+ *
+ * @see #getVariable(String) to look up a global or route variable by its
prefixed name
+ */
+ public Map<String, Object> getVariables() {
+ return exchange.getVariables();
+ }
+
+ /**
+ * The variable with the given name, which can be prefixed with {@code
global:} or {@code route:} for a global or
+ * route variable.
+ */
+ public Object getVariable(String name) {
+ return exchange.getVariable(name);
+ }
+
+ /**
+ * The variable with the given name converted to the given type, which can
be prefixed with {@code global:} or
+ * {@code route:} for a global or route variable.
+ */
+ public <T> T getVariable(String name, Class<T> type) {
+ return exchange.getVariable(name, type);
+ }
+
}
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 34c6c60689ce..ae3b3396c258 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
@@ -2011,6 +2011,14 @@ polyglot `Value` itself needs to work with these types
instead.
`JavaScriptHelper.newContext()` is deprecated: contexts are created by the
language from its shared engine.
+=== camel-javascript, camel-python - script variables use the Groovy names
+
+The `js` and `python` languages now bind the script variables with the names
the `groovy` language uses:
+`camelContext`, `request`, `header`, `exchangeProperty`, `exchangeProperties`
and `exception` are new, next to
+`exchange`, `exchangeId`, `message`, `body`, `headers`, `variable` and
`variables`. In `js` and `python` the
+`context` and `properties` variables are deprecated in favour of
`camelContext` and `exchangeProperties`; they still
+work. The new `python3` language binds only the Groovy names, and in its
default data-only mode only the data variables.
+
=== camel-rest, camel-rest-openapi, camel-jbang - camel-http is the default
REST client
When a REST producer (`rest:` in producer mode, or `rest-openapi` calling an
operation of a contract) has no