allthingssecurity opened a new pull request, #27525: URL: https://github.com/apache/camel/pull/27525
# Description [CAMEL-24467](https://issues.apache.org/jira/browse/CAMEL-24467) Groovy and Jactl bind Exchange variables as `variable` and `variables`, the names `ExchangeHelper.populateVariableMap` uses (camel-quickjs binds `variables` as well). The other scripting languages did not bind them. In JavaScript, MVEL, OGNL and Jython a script could only reach them through `exchange.getVariable(...)`. In python3's default data-only mode there was no way to read them at all, because `exchange` is not bound there. This change implements the scope I proposed on the ticket on 2026-09-25 (and the correction posted the same day). It is additive: no existing binding or method is renamed or removed. - **js** (`JavaScriptExpression`): binds `variable` and `variables` to `exchange.getVariables()`, next to `headers` and `properties`. `JavaScriptExpression` is the only place the language binds Exchange data. `JavaScriptLanguage.evaluate(script, bindings, type)` (the `ScriptingLanguage` entry point) takes its bindings from the caller, so it is unchanged. - **python** (Jython, `PythonExpression`): the same, `variable` and `variables`. - **mvel** and **ognl** (`RootObject`): add `getVariables()`, `getVariable(String)` and `getVariable(String, Class<T>)`, mirroring `getHeaders()`/`getHeader(...)`. In both languages a root-object method is called by its full name: `getHeader('foo')` works, but `header('foo')` does not resolve. So the variable getter is `getVariable('foo')`, the same form that works for headers. The existing docs tables of both languages listed `header(name)`, `header(name, type)`, `property(name)` and `property(name, type)`, which never resolved; this PR corrects them to `getHeader(...)`/`getProperty(...)`. Correction to my earlier comment on CAMEL-24467: I wrote that `variable('foo')` would work in MVEL/OGNL like `header('foo')`. Neither does; the working forms are `getVariable('foo')` and `getHeader('foo')`. `getVariable` goes through `Exchange.getVariable`, so it also resolves `global:` and `route:` variables. The MVEL *component* (templates) already had variables through `ExchangeHelper.createVariableMap`. - **spel** (camel-spring `RootObject`): the same three getters as MVEL and OGNL, so `#{variables.foo}`, `#{variables['foo']}`, `#{getVariable('foo')}` and `#{getVariable('num', T(Integer))}` work. Its docs table had the same `header(name)`/`property(name)` rows, which fail (`EL1004E: Method call: Method header(java.lang.String) cannot be found on type org.apache.camel.language.spel.RootObject`); they now read `getHeader(...)`/`getProperty(...)` as well. These getters are not SpEL's `#name` evaluation-context variables, which Camel does not set. - **python3** (GraalPy): binds `variables` as a data binding in exactly the way `headers` is bound. It is bound in both the default and the trusted (`createWithHostAccess()`) mode, because `headers` is bound in both. Host access is not widened. In default mode, Python can index the map but cannot call Java methods on it (`variables.put(...)` and `variables.getClass()` raise `AttributeError`). Only `variables` is added, with no `variable` alias, matching python3's existing names (`headers`, no `header`) and camel-quickjs, the other sandboxed language. In default mode, global and route variables cannot be read (there is no `exchange`), and an assignment through the map does not go through `Exchange.setVariable`, so `variables['global:x'] = 1` creates an exchange variable literally named `global:x`; the docs say both. Like `headers`, it is the exchange's own map rather than a copy, so `variables['foo'] = 'bar'` in a script updates the exchange. One difference from `headers`: the vari ables map is a `ConcurrentHashMap`, so `variables['foo'] = None` fails with a `NullPointerException`. The docs mention it. The `variables` map only contains exchange-scoped variables. `global:`/`route:` variables are reachable through `getVariable` in MVEL/OGNL and through `exchange.getVariable(...)` in js, python and trusted python3, and the docs say so. The language metadata JSON (`js.json`, `mvel.json`, ...) does not list bound variables, so it needs no change. Examples: ```java // JavaScript .setBody().js("variables.greeting + ' ' + variable.get('name')") .filter().js("variables.name == 'Camel'") // Jython .filter().python("variables['name'] == 'Camel'") // MVEL / OGNL .filter().mvel("variables.name == 'Camel'") .setHeader("n").mvel("getVariable('num', Integer) + 1") .setHeader("n").ognl("getVariable('num', @java.lang.Integer@class) + 1") .setHeader("g").ognl("getVariable('global:greeting')") // Python 3 (default, data-only mode) .setBody().python3("f\"{variables['greeting']} {variables['name']}\"") .filter().python3("variables['name'] == 'Camel'") ``` Cost: `exchange.getVariables()` creates the exchange's variable store, so js, python and python3 bind the variables only when the script text names them (`variable` or `variables`; `variables` for python3), checked once when the expression is created. A script that does not use variables creates no store and costs the same as before (Groovy and Jactl also create the store only on use). A script that reaches the binding through a computed name (such as `globals()['vari' + 'ables']`) does not see it; `exchange.getVariables()` still works in js, python and trusted python3. The root-object getters of MVEL, OGNL and SpEL only run when an expression uses them. A shared definition of the bound names, and a common facade for them across languages, are left for a follow-up. Federico Mariani suggested this on CAMEL-24467 (2026-09-11, from the review of the camel-quickjs `camel` facade in CAMEL-24688 / #26309): one place in camel-support that lists the binding names and the facade operations (`getBody/setBody`, `get/set/removeHeader`, `get/set/removeProperty`, `get/set/removeVariable`, `log`), so that jactl and python3 could offer mutation without trusted mode and every scripting language's docs could point to the same table. This PR only covers the bindings side. Tests (one new package-private JUnit 5 class per component, using the module's assertion style): - `JavaScriptVariablesTest`, `PythonVariablesTest`: `variables`/`variable` in expressions and predicates, and a route that sets variables with the Set Variable EIP and reads them in `setBody` and a `choice`. - `MvelVariablesTest`, `OgnlVariablesTest`: `variables.foo`, `variables['foo']`, `getVariable('foo')`, the typed getter (a `"123"` variable read as an `Integer`, so `+ 1` gives `124` and not `"1231"`), `getVariable('global:...')`, and the same route. - `Python3VariablesTest`: default mode reads the map, writes propagate like `headers`, Java methods on the map are denied, trusted mode also binds `variables`, and the same route. - `SpelVariablesTest` (camel-spring-xml, where the SpEL tests live): `#{variables.foo}`, `#{variables['foo']}`, `#{getVariable('foo')}`, the typed getter and `getVariable('global:...')`. - `variableStoreIsOnlyCreatedForScriptsThatNameTheVariables` in the js, python and python3 classes: through an `Exchange` proxy that counts `getVariables()` calls, a script without variables makes none and a script with them makes one. These three pass on main (where nothing is bound); they fail with the earlier version of this change that bound the variables up front (`a script without variables should not create the variable store ==> expected: <0> but was: <1>`). Rebased on `main` (374c04877418, 2026-10-05). `main` has not changed the binding code of these five languages since 2026-09-25 (the only commit touching them is the docs-tabs change CAMEL-25265 in the python3 page, which rebased cleanly). Without the main-code change, every new test fails (with surefire's reruns, re-checked after the rebase): ``` JavaScriptVariablesTest.variablesAreBound » Polyglot ReferenceError: variables is not defined MvelVariablesTest.variablesAreBound » ExpressionEvaluation [Error: could not access: variables; in class: org.apache.camel.language.mvel.RootObject] MvelVariablesTest.typedVariableGetterConverts » ExpressionEvaluation [Error: unable to resolve method: org.apache.camel.language.mvel.RootObject.getVariable(java.lang.String, java.lang.Class) ...] OgnlVariablesTest.variablesAreBound » ExpressionEvaluation ognl.NoSuchPropertyException: org.apache.camel.language.ognl.RootObject.variables OgnlVariablesTest.typedVariableGetterConverts » ExpressionEvaluation ognl.MethodFailedException: Method "getVariable" failed for object org.apache.camel.language.ognl.RootObject@... PythonVariablesTest.variablesAreBound » ExpressionIllegalSyntax Illegal syntax: variables['foo'] Python3VariablesTest.defaultModeBindsVariablesAsData » ExpressionEvaluation NameError: name 'variables' is not defined Python3VariablesTest.defaultModeDeniesJavaMethodsOnVariables [variables.put('k', 'v') should mention "AttributeError" but was: ... NameError: name 'variables' is not defined] ``` js 2/2, mvel 4/4, ognl 4/4, python 2/2 and python3 5/5 tests failed. With main's SpEL `RootObject`, the 3 `SpelVariablesTest` tests fail too (`EL1008E: Property or field 'variables' cannot be found on object of type 'org.apache.camel.language.spel.RootObject'`, `EL1004E: Method call: Method getVariable(java.lang.String) cannot be found ...`). The route tests fail with a `CamelExecutionException` from the same errors. With the change, the full test suites pass: camel-javascript 17, camel-mvel 17, camel-ognl 16, camel-python 12, camel-python3 49 and camel-spring-xml 1171 (26 skipped) tests, 0 failures, 0 errors (camel-spring built in the same reactor). The language docs list the new bindings, and the mirrored copies under `catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/` were regenerated with `prepare-catalog` (they are byte-identical copies). Rebuilding the changed modules left no uncommitted changes. Not changed, per the ticket: camel-groovy, camel-jactl and camel-quickjs already bind variables, and camel-joor exposes `exchange`, so `exchange.getVariable(...)` works there. There is no camel-jexl module on `main`. Follow-up, not changed: the MVEL, OGNL and SpEL tables also say `*this*` is the Exchange, but the root object is the `RootObject` wrapper (`#{#root}` in SpEL is a `RootObject`); `exchange` is the Exchange. # Target - [x] I checked that the commit is targeting the correct branch (Camel 4 uses the `main` branch) # Tracking - [x] If this is a large change, bug fix, or code improvement, I checked there is a [JIRA issue](https://issues.apache.org/jira/browse/CAMEL) filed for the change (usually before you start working on it). # Apache Camel coding standards and style - [x] I checked that each commit in the pull request has a meaningful subject line and body. - [ ] I have run `mvn clean install -DskipTests` locally from root folder and I have committed all auto-generated changes. (I built and tested the affected modules, including the formatter and import-sort plugins, and regenerated the catalog docs. I did not run the full root build.) # AI-assisted contributions - [x] If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., `Co-authored-by` trailers) and the PR description identifies the AI tool used. This PR was prepared with Claude Code (Claude Opus 5.5). The commit carries a `Co-Authored-By` trailer. _Claude Code on behalf of allthingssecurity_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
