| = QuickJS Language |
| :doctitle: QuickJS |
| :shortname: quickjs |
| :artifactid: camel-quickjs |
| :description: Evaluates a JavaScript expression using QuickJS4J |
| :since: 4.23 |
| :supportlevel: Preview |
| :tabs-sync-option: |
| |
| *Since Camel {since}* |
| |
| The QuickJS language evaluates JavaScript as an xref:manual::expression.adoc[Expression] or |
| xref:manual::predicate.adoc[Predicate] in Camel routes. |
| |
| `camel-javascript` provides GraalVM JavaScript with Java interoperability, while `camel-quickjs` |
| provides a lightweight pure-Java JavaScript runtime using QuickJS4J and JSON-based data exchange. |
| |
| QuickJS4J compiles QuickJS to WebAssembly and runs it as Java bytecode through Endive (the successor |
| to Chicory). There is no JNI and no native library. Native-image compatibility and |
| architecture-specific support have not been validated as part of this module. |
| |
| Do not treat this language as a drop-in replacement for xref:js-language.adoc[JavaScript]. The |
| scripting APIs and Exchange bindings are different. |
| |
| For example, you can use QuickJS in a xref:manual::predicate.adoc[Predicate] with the |
| xref:eips:choice-eip.adoc[Content-Based Router] EIP. |
| |
| == QuickJS Options |
| |
| // language options: START |
| include::partial$language-options.adoc[] |
| // language options: END |
| |
| == Variables |
| |
| The following variables are bound for each evaluation. Values are JSON snapshots taken before the |
| script runs, not live Java objects. Live `Exchange`, `Message` and `CamelContext` instances are |
| never exposed; the `camel` API below is the way to read the current state or to change it. |
| |
| [width="100%",cols="10%,10%,80%",options="header",] |
| |======================================================================= |
| |Variable |Type |Description |
| |body |JSON value |the message body after JSON conversion |
| |headers |Object |the message headers after JSON conversion |
| |properties |Object |the exchange properties after JSON conversion |
| |exchangeId |String |the exchange id |
| |variables |Object |the exchange variables after JSON conversion (an empty object when there are none) |
| |exception |Object or null |`null`, or `{ type, message }` of the exception on the exchange (or the caught exception in an error handler) |
| |camel |Object |the controlled Camel API, see below |
| |======================================================================= |
| |
| `message`, `exchange`, and `context` are not bound. Scripts that refer to them raise a JavaScript |
| `ReferenceError`. |
| |
| Assigning to `headers`, `properties`, `variables` or `body` inside a script changes only the |
| JavaScript snapshot. It does not mutate the Camel `Exchange`. Use the `camel` API, or the |
| expression result (for example `.transform().quickjs(...)`) when you need to change the message. |
| |
| === The camel API |
| |
| `camel` is a frozen object whose functions read and write the *current* exchange through |
| QuickJS4J host functions. Arguments and results cross the boundary as JSON, so a value written |
| with `camel.setHeader` is stored as a `String`, `Number`, `Boolean`, `List` or `Map`. |
| |
| [width="100%",cols="30%,70%",options="header",] |
| |======================================================================= |
| |Function |Description |
| |`camel.getBody()` |the current message body (JSON snapshot; a streaming body raises the same error as the `body` binding) |
| |`camel.setBody(value)` |replaces the message body |
| |`camel.getHeader(name)` |a header of the current message (JSON snapshot) |
| |`camel.setHeader(name, value)` |sets a header on the current message |
| |`camel.removeHeader(name)` |removes a header and returns its previous value |
| |`camel.getProperty(name)` / `camel.setProperty(name, value)` / `camel.removeProperty(name)` |the same for exchange properties |
| |`camel.getVariable(name)` / `camel.setVariable(name, value)` / `camel.removeVariable(name)` |the same for exchange variables |
| |`camel.log(level, message)` |logs through the `org.apache.camel.language.quickjs.script` logger at `trace`, `debug`, `info`, `warn` or `error` |
| |======================================================================= |
| |
| [tabs] |
| ==== |
| Java:: |
| + |
| [source,java] |
| ---- |
| from("direct:start") |
| .setBody().quickjs("camel.setHeader('processed', true); body.toUpperCase()") |
| .to("mock:result"); |
| ---- |
| |
| XML:: |
| + |
| [source,xml] |
| ---- |
| <route> |
| <from uri="direct:start"/> |
| <setBody> |
| <quickjs>camel.setHeader('processed', true); body.toUpperCase()</quickjs> |
| </setBody> |
| <to uri="mock:result"/> |
| </route> |
| ---- |
| |
| YAML:: |
| + |
| [source,yaml] |
| ---- |
| - route: |
| from: |
| uri: direct:start |
| steps: |
| - setBody: |
| expression: |
| quickjs: |
| expression: 'camel.setHeader(''processed'', true); body.toUpperCase()' |
| - to: |
| uri: mock:result |
| ---- |
| ==== |
| |
| The `camel` API is only available while a route expression is evaluated; the generic |
| `ScriptingLanguage.evaluate(script, bindings, resultType)` entry point has no current exchange. |
| |
| === Expressions and statements |
| |
| A script that is a single expression (`body.amount > 100`, `{ a: body }`) is compiled as an |
| expression and returns its value. Any other script (several statements, a trailing semicolon, a |
| `var` declaration) is evaluated as statements and returns its completion value, the value of the |
| last statement, exactly as `eval` would. Note that `{ a: 1 }` is therefore an object when it is |
| the whole script but a block statement when it is followed by other statements. |
| |
| The generic `ScriptingLanguage.evaluate(script, bindings, resultType)` API uses caller-supplied |
| map keys as JavaScript function parameters. Those keys must be valid JavaScript identifiers |
| (for example `body` or `foo_bar`). Names such as `foo-bar`, `123foo`, or reserved words such as |
| `for` are rejected with a Camel evaluation exception rather than a raw JavaScript `SyntaxError`. |
| Route expressions do not use this map. |
| |
| == Data types |
| |
| Values cross the Java/JavaScript boundary as JSON: |
| |
| * `null`, string, boolean, and number pass through. |
| * `Map` becomes a JavaScript object. Header and property names are strings, so |
| `headers.MyHeader` and `headers['MyHeader']` both work. |
| * `List` and arrays become JavaScript arrays. |
| * `byte[]` becomes a Base64 JSON string. `char[]` becomes a JSON string. |
| * Other Java types are serialized with Jackson into a JSON object or array snapshot. |
| Java methods such as `getAge()` are not callable from JavaScript; use JSON fields |
| such as `body.age`. |
| * `Exchange`, `Message`, `CamelContext`, `Class`, and `ClassLoader` values are rejected |
| when they appear as the message body, with an evaluation error. They are never passed |
| into the script. |
| * Streaming bodies (`InputStream`, `Reader`, and Camel `StreamCache`) are rejected with |
| an evaluation error. The stream is not read, closed, or otherwise consumed. |
| * Header and property values that cannot be JSON-serialized (including Camel internals |
| and streaming values) are omitted from the JavaScript snapshot so evaluation can still |
| use the remaining data. |
| |
| Serialization failures of the message body raise a Camel evaluation exception that names |
| the unsupported type. |
| |
| == Expression and predicate |
| |
| As an expression, the JavaScript value of the script becomes the Camel result (then converted |
| with Camel type converters when a result type is requested). |
| |
| As a predicate (`.when().quickjs(...)` or `.filter().quickjs(...)`), the result is converted to |
| boolean with Camel's standard `ObjectHelper.evaluateValuePredicate` rules: a `Boolean` is used |
| directly; the strings `true`/`false` are parsed; any other non-empty, non-null value is true. |
| |
| == Engine lifecycle |
| |
| Every worker thread owns one QuickJS engine, created on first use and closed when the language stops. Each |
| engine keeps the last 1,000 route expressions it evaluated in compiled form, so a script is compiled once per |
| thread and then only executed. A JavaScript exception thrown by a script leaves the engine usable; a trap inside |
| the runtime (a `camel` function that failed, a stack overflow) does not, and the engine of that thread is then |
| discarded and recreated on the next evaluation. |
| QuickJS keeps every module it has evaluated until its context is freed, and QuickJS4J evaluates a |
| module per call, so an engine grows with every evaluation. The language therefore recycles a |
| thread's engine once its WebAssembly memory exceeds `engineMaxMemory` (64 MB) or it has run |
| `engineMaxEvaluations` (50,000) evaluations. Both are properties of `QuickjsLanguage` and can be set like any |
| language option, for example in `application.properties`: |
| |
| [source,properties] |
| ---- |
| camel.language.quickjs.engineMaxMemory = 134217728 |
| camel.language.quickjs.engineMaxEvaluations = 100000 |
| ---- |
| |
| or programmatically through `((QuickjsLanguage) context.resolveLanguage("quickjs")).setEngineMaxMemory(...)`. |
| |
| == Security |
| |
| JavaScript runs in the QuickJS4J sandbox. The runtime does not expose Java classes, reflection, |
| class loaders, or live Camel objects. WASI has no filesystem or network preopens. Stdout from |
| scripts is discarded so a reused engine does not accumulate output. Per-evaluation stderr is |
| captured into Camel exceptions (for example `ReferenceError`) and then cleared so later |
| evaluations do not include stale error output. |
| |
| QuickJS4J host plumbing is not available to user scripts: `java_invoke` throws a |
| `TypeError` and `quickjs4j_engine` is undefined, including when accessed through |
| `globalThis`. The `camel` API is the only host bridge scripts can reach, and it only |
| dispatches to the functions listed above. |
| |
| == Usage |
| |
| [tabs] |
| ==== |
| Java:: |
| + |
| [source,java] |
| ---- |
| import static org.apache.camel.language.quickjs.QuickjsLanguage.quickjs; |
| |
| public class MyRouteBuilder extends RouteBuilder { |
| @Override |
| public void configure() { |
| from("direct:start") |
| .choice() |
| .when().quickjs("headers.MyHeader == 'foo'").to("mock:foo") |
| .otherwise().to("mock:other"); |
| } |
| } |
| ---- |
| |
| XML:: |
| + |
| [source,xml] |
| ---- |
| <route> |
| <from uri="direct:start"/> |
| <choice> |
| <when> |
| <quickjs>headers.MyHeader == 'foo'</quickjs> |
| <to uri="mock:foo"/> |
| </when> |
| <otherwise> |
| <to uri="mock:other"/> |
| </otherwise> |
| </choice> |
| </route> |
| ---- |
| |
| YAML:: |
| + |
| [source,yaml] |
| ---- |
| - route: |
| from: |
| uri: direct:start |
| steps: |
| - choice: |
| when: |
| - expression: |
| quickjs: |
| expression: 'headers.MyHeader == ''foo''' |
| steps: |
| - to: |
| uri: mock:foo |
| otherwise: |
| steps: |
| - to: |
| uri: mock:other |
| ---- |
| ==== |
| |
| Transform the body with the expression result: |
| |
| [tabs] |
| ==== |
| Java:: |
| + |
| [source,java] |
| ---- |
| from("direct:start") |
| .transform().quickjs("body.toUpperCase()") |
| .to("mock:result"); |
| ---- |
| |
| XML:: |
| + |
| [source,xml] |
| ---- |
| <route> |
| <from uri="direct:start"/> |
| <transform> |
| <quickjs>body.toUpperCase()</quickjs> |
| </transform> |
| <to uri="mock:result"/> |
| </route> |
| ---- |
| |
| YAML:: |
| + |
| [source,yaml] |
| ---- |
| - route: |
| from: |
| uri: direct:start |
| steps: |
| - transform: |
| expression: |
| quickjs: |
| expression: body.toUpperCase() |
| - to: |
| uri: mock:result |
| ---- |
| ==== |
| |
| You can load the script from an external resource with the |
| `resource:scheme:location` syntax, for example `resource:classpath:myscript.js` |
| or `resource:file:/path/to/script.js`. |
| |
| [WARNING] |
| ==== |
| Do not derive `resource:classpath:` or `resource:file:` paths from untrusted |
| input such as message headers or query parameters. A path taken from untrusted |
| data can cause Camel to load and evaluate an unexpected JavaScript file. |
| ==== |
| |
| == Dependencies |
| |
| To use QuickJS in your Camel routes, you need to add the dependency on |
| *camel-quickjs*. |
| |
| QuickJS4J is licensed under Apache License 2.0 (ASF Category A). |
| |
| If you use Maven, you could add the following to your `pom.xml`, |
| substituting the version number for the latest release. |
| |
| [source,xml] |
| ---- |
| <dependency> |
| <groupId>org.apache.camel</groupId> |
| <artifactId>camel-quickjs</artifactId> |
| <version>x.x.x</version> |
| </dependency> |
| ---- |