From 82e67c13a98f3d72d9d838b62687aeb89f8b7fa3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 19:57:22 +0000 Subject: [PATCH 1/5] Document GraalJS Java interop differences GraalJS does not convert values at the Java boundary the way Nashorn did, and developers hit the same handful of failures each time. Add a short "Java interop" section to the engines page covering the four rules: convert with __.toScriptValue/__.toNativeObject, call setters instead of assigning properties, pass the declared parameter type, and do not type-check Java values from JavaScript. Note that XP's bridge is __ rather than an engine's own Java global, and link the GraalJS reference with the caveat that XP does not expose everything it documents. Fix the Java bridge page accordingly: its parameter example used the Nashorn-only property assignment, which fails on GraalJS - it now calls setters, the way the platform libraries do. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dx8tfKuJaZd2rDvkF8Aoba --- docs/runtime/engines.adoc | 14 ++++++++++++++ docs/runtime/java-bridge.adoc | 14 ++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/runtime/engines.adoc b/docs/runtime/engines.adoc index a733d5f3..898db0b1 100644 --- a/docs/runtime/engines.adoc +++ b/docs/runtime/engines.adoc @@ -59,6 +59,20 @@ ES modules:: *Not supported as a loading mechanism.* XP loads every file as a Co Node.js APIs:: *Not supported.* No `process`, `Buffer` or `global`, and no `node_modules` resolution. Web APIs:: *Partially supported.* `TextEncoder` and `TextDecoder` are available. Further APIs may become available in future GraalJS versions, while others, such as `fetch` and `setTimeout`, might never be supported. For HTTP, mail, storage and the rest, use the platform <<../libraries#, libraries>>. +[#graaljs-interop] +=== Java interop + +The <> is the same on both engines - the same `+__+` object, the same beans. What differs is that Nashorn converted values for you at the boundary and GraalJS does not. Four rules cover it: + +Convert every value that crosses the boundary:: `+__.toScriptValue()+` on the way in, `+__.toNativeObject()+` on the way out - which is how the platform <<../libraries#, libraries>> are written. A Java `Map` returned straight to JavaScript stays a Java object: `Object.keys()`, spread and `JSON.stringify()` see its class methods rather than its entries, and raise nothing. A single property read may still answer, which is what makes the mistake easy to miss. Java `List` returns behave as arrays on both engines. +Call setters, do not assign properties:: `bean.setValue(x)`, never `bean.value = x`. Nashorn rewrote that assignment into the setter call; on GraalJS it fails with `Unknown identifier`. +Pass the type the method declares:: Arguments are not coerced. A JavaScript string handed to a Java `int` parameter throws. Request parameters always arrive as strings, so parse them before they reach Java. +Do not branch on a Java value's type from JavaScript:: `+Object.prototype.toString.call(value)+` named the Java class on Nashorn; on GraalJS it answers `+[object Object]+`. Convert the value first, then inspect what you got back. + +NOTE: XP's bridge is `+__+`, on both engines. The interop globals an engine installs itself - Nashorn's `Java` object among them - are not part of XP's API, and code written against them is not portable across engines. + +GraalJS has its own https://www.graalvm.org/javascript/docs/[documentation], which describes the engine as it ships standalone. XP embeds it, so read that reference together with <> above and with the rules on this page: what the engine can do and what an XP app may rely on are not the same list. + [#contexts] === Script contexts diff --git a/docs/runtime/java-bridge.adoc b/docs/runtime/java-bridge.adoc index 9a01e2df..3d96d161 100644 --- a/docs/runtime/java-bridge.adoc +++ b/docs/runtime/java-bridge.adoc @@ -49,7 +49,7 @@ Java objects constructed by `newBean` may, but not required to, implement `com.e There are 2 ways to pass parameters to a Java method, from JavaScript: - Passing the parameters in the method call -- Setting the parameters as properties in the Java object, and then calling the method without parameters +- Calling a setter on the Java object for each parameter, and then calling the method without parameters The first one is recommended when there are few parameters (1 or 2) and of simple types. The second one is better when there are multiple parameters, or some of them are optional. @@ -65,8 +65,8 @@ exports.doSomething = function (param1, param2) { exports.doSomethingElse = function (params) { var bean = __.newBean('com.enonic.lib.mylib.MyClass'); - bean.text = __.nullOrValue(params.text) || ''; - bean.size = __.nullOrValue(params.size) || 250; + bean.setText(__.nullOrValue(params.text) || ''); + bean.setSize(__.nullOrValue(params.size) || 250); return bean.execute(); }; @@ -75,7 +75,9 @@ exports.doSomethingElse = function (params) { NOTE: When passing values that might be `null` or `undefined` it is recommended to filter them using the `__.nullOrValue` built-in function. This function converts any value that is `null` or `undefined` in JavaScript to `null` in Java. Otherwise returns the input value without changes. -To be able to set property values as in the 2nd example above, the Java object must implement a setter method for each field. +The Java object must implement a setter method for each field set that way. + +WARNING: Call the setter, as above - do not assign to the field as if it were a property. Nashorn rewrote `bean.text = value` into `bean.setText(value)`; on GraalJS the assignment fails with `Unknown identifier`. See <>. The Java class used in the example above looks like this: @@ -116,11 +118,15 @@ There are some type conversions that are made when calling from JavaScript to Ja - when passing a JavaScript `array`, the Java method should expect a Java `List` - when passing a JavaScript `object`, the Java method should expect a Java `Map` +The argument must already be of the type the method declares - it is not coerced on the way in. A JavaScript string passed to a Java `int` or `Long` parameter throws on GraalJS, so parse values that arrive as strings, such as request parameters, before the call. Wrap objects and arrays in `+__.toScriptValue()+` to hand them over as a Java `Map` or `List`. + === Returning results from Java When returning simple values from Java to a JavaScript caller, the same type conversions applies. +Complex values are *not* converted: a Java `Map` returned to JavaScript is still a Java object, and inspecting it with `Object.keys()`, spread or `JSON.stringify()` reports the class methods rather than the entries. Convert it once, in the module that makes the call, with `+__.toNativeObject()+`, as the `readLines` example above does. See <>. + To return complex object values, you should create a specific Java class to make the mapping. This class should implement the ``MapSerializable`` interface. It will implement the ``serialize`` method, which allows generating a JSON object. From c3116f05dec0ab771421917bc1631dcb210f5fdb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 20:06:15 +0000 Subject: [PATCH 2/5] Frame HTTP parameters as an example of the coercion rule Arguments are never coerced; HTTP request parameters arriving as strings are one instance of that, not the rule itself. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dx8tfKuJaZd2rDvkF8Aoba --- docs/runtime/engines.adoc | 2 +- docs/runtime/java-bridge.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/runtime/engines.adoc b/docs/runtime/engines.adoc index 898db0b1..7b1bb143 100644 --- a/docs/runtime/engines.adoc +++ b/docs/runtime/engines.adoc @@ -66,7 +66,7 @@ The <> is the same on both engines - the same `+__+` Convert every value that crosses the boundary:: `+__.toScriptValue()+` on the way in, `+__.toNativeObject()+` on the way out - which is how the platform <<../libraries#, libraries>> are written. A Java `Map` returned straight to JavaScript stays a Java object: `Object.keys()`, spread and `JSON.stringify()` see its class methods rather than its entries, and raise nothing. A single property read may still answer, which is what makes the mistake easy to miss. Java `List` returns behave as arrays on both engines. Call setters, do not assign properties:: `bean.setValue(x)`, never `bean.value = x`. Nashorn rewrote that assignment into the setter call; on GraalJS it fails with `Unknown identifier`. -Pass the type the method declares:: Arguments are not coerced. A JavaScript string handed to a Java `int` parameter throws. Request parameters always arrive as strings, so parse them before they reach Java. +Pass the type the method declares:: Arguments are not coerced. A JavaScript string handed to a Java `int` parameter throws. For instance, HTTP request parameters always arrive as strings, so parse them before they reach Java. Do not branch on a Java value's type from JavaScript:: `+Object.prototype.toString.call(value)+` named the Java class on Nashorn; on GraalJS it answers `+[object Object]+`. Convert the value first, then inspect what you got back. NOTE: XP's bridge is `+__+`, on both engines. The interop globals an engine installs itself - Nashorn's `Java` object among them - are not part of XP's API, and code written against them is not portable across engines. diff --git a/docs/runtime/java-bridge.adoc b/docs/runtime/java-bridge.adoc index 3d96d161..79bc7794 100644 --- a/docs/runtime/java-bridge.adoc +++ b/docs/runtime/java-bridge.adoc @@ -118,7 +118,7 @@ There are some type conversions that are made when calling from JavaScript to Ja - when passing a JavaScript `array`, the Java method should expect a Java `List` - when passing a JavaScript `object`, the Java method should expect a Java `Map` -The argument must already be of the type the method declares - it is not coerced on the way in. A JavaScript string passed to a Java `int` or `Long` parameter throws on GraalJS, so parse values that arrive as strings, such as request parameters, before the call. Wrap objects and arrays in `+__.toScriptValue()+` to hand them over as a Java `Map` or `List`. +The argument must already be of the type the method declares - it is not coerced on the way in. A JavaScript string passed to a Java `int` or `Long` parameter throws on GraalJS. For instance, HTTP request parameters always arrive as strings, so parse them before the call. Wrap objects and arrays in `+__.toScriptValue()+` to hand them over as a Java `Map` or `List`. === Returning results from Java From bd01fe7943e5a63f5f77953b6d84c21ad093f2c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 20:10:05 +0000 Subject: [PATCH 3/5] Rework the Java interop guidance into prose plus examples Formalise the wording and move the identifiers out of the sentences: each of the four rules is now a subsection with a TypeScript example showing the supported form beside the one that fails, rather than a labelled list carrying the code inline. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dx8tfKuJaZd2rDvkF8Aoba --- docs/runtime/engines.adoc | 73 +++++++++++++++++++++++++++++++---- docs/runtime/java-bridge.adoc | 28 ++++++++++++-- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/docs/runtime/engines.adoc b/docs/runtime/engines.adoc index 7b1bb143..a5a2952a 100644 --- a/docs/runtime/engines.adoc +++ b/docs/runtime/engines.adoc @@ -62,16 +62,75 @@ Web APIs:: *Partially supported.* `TextEncoder` and `TextDecoder` are available. [#graaljs-interop] === Java interop -The <> is the same on both engines - the same `+__+` object, the same beans. What differs is that Nashorn converted values for you at the boundary and GraalJS does not. Four rules cover it: +The <> is identical on both engines: the same bridge object, the same beans, the same libraries. What differs is conversion. Nashorn converted values implicitly as they crossed between JavaScript and Java, whereas GraalJS converts nothing. Four rules follow from that, and code written to observe them runs unchanged on either engine. -Convert every value that crosses the boundary:: `+__.toScriptValue()+` on the way in, `+__.toNativeObject()+` on the way out - which is how the platform <<../libraries#, libraries>> are written. A Java `Map` returned straight to JavaScript stays a Java object: `Object.keys()`, spread and `JSON.stringify()` see its class methods rather than its entries, and raise nothing. A single property read may still answer, which is what makes the mistake easy to miss. Java `List` returns behave as arrays on both engines. -Call setters, do not assign properties:: `bean.setValue(x)`, never `bean.value = x`. Nashorn rewrote that assignment into the setter call; on GraalJS it fails with `Unknown identifier`. -Pass the type the method declares:: Arguments are not coerced. A JavaScript string handed to a Java `int` parameter throws. For instance, HTTP request parameters always arrive as strings, so parse them before they reach Java. -Do not branch on a Java value's type from JavaScript:: `+Object.prototype.toString.call(value)+` named the Java class on Nashorn; on GraalJS it answers `+[object Object]+`. Convert the value first, then inspect what you got back. +==== Convert values at the boundary -NOTE: XP's bridge is `+__+`, on both engines. The interop globals an engine installs itself - Nashorn's `Java` object among them - are not part of XP's API, and code written against them is not portable across engines. +Values are converted explicitly: on the way into Java with `toScriptValue`, and on the way back out with `toNativeObject`. This is how the platform <<../libraries#, libraries>> are written. -GraalJS has its own https://www.graalvm.org/javascript/docs/[documentation], which describes the engine as it ships standalone. XP embeds it, so read that reference together with <> above and with the rules on this page: what the engine can do and what an XP app may rely on are not the same list. +[source,typescript] +---- +const bean = __.newBean('com.example.MyBean'); + +bean.setConfig(__.toScriptValue(params.config)); + +const result = __.toNativeObject(bean.execute()); +---- + +A Java map returned without conversion remains a Java object. Inspecting it with the standard JavaScript operations reports the methods of its class instead of its entries, and reports no error in doing so. An individual property read may still return the expected value, which is what makes the omission easy to overlook. + +[source,typescript] +---- +const map = bean.getConfig(); // a Java Map, unconverted + +const mode = map.mode; // 'live' - the read succeeds +const keys = Object.keys(map); // ['get', 'put', 'size', ...] +const json = JSON.stringify(map); // '{}' +const copy = {...map}; // {} + +const config = __.toNativeObject(bean.getConfig()); // a JavaScript object +---- + +Java lists are the exception. They present array semantics on both engines, so index access and iteration require no conversion. + +==== Call setters rather than assigning properties + +Nashorn rewrote an assignment to a bean property into a call to the corresponding setter. GraalJS does not, and the assignment fails with an `Unknown identifier` error. + +[source,typescript] +---- +bean.setValue(42); // supported on both engines +bean.value = 42; // Nashorn only; fails on GraalJS +---- + +TypeScript declarations written for a bean should therefore declare its setters and omit the properties, so that the unsupported form does not compile. + +==== Pass the type the method declares + +Arguments are not coerced to the declared parameter type. HTTP request parameters, for instance, always arrive as strings and must be parsed before they reach a Java method that expects a number. + +[source,typescript] +---- +bean.setCount(req.params.count); // fails: a string is not an int +bean.setCount(parseInt(req.params.count, 10)); // supported on both engines +---- + +==== Do not determine Java types from JavaScript + +The standard type-tag idiom reported the name of the Java class on Nashorn. On GraalJS it reports a generic tag, so a branch written against that string stops matching, and it does so silently. + +[source,typescript] +---- +Object.prototype.toString.call(value); +// Nashorn: '[object com.example.Thing]' +// GraalJS: '[object Object]' +---- + +Convert the value at the boundary and branch on the converted result, or perform the type test in Java, where the type is known. + +NOTE: The bridge XP supports is the `+__+` object, on both engines. Interop globals installed by an engine itself, among them the `Java` object, are not part of the XP API, and code written against them is not portable between engines. + +GraalJS is documented by its maintainers at https://www.graalvm.org/javascript/docs/[graalvm.org]. That reference describes the engine as it is distributed standalone; XP embeds it, so it should be read together with <> above. What the engine is capable of and what an application may rely on are not the same set. [#contexts] === Script contexts diff --git a/docs/runtime/java-bridge.adoc b/docs/runtime/java-bridge.adoc index 79bc7794..ae48eef4 100644 --- a/docs/runtime/java-bridge.adoc +++ b/docs/runtime/java-bridge.adoc @@ -77,7 +77,16 @@ This function converts any value that is `null` or `undefined` in JavaScript to The Java object must implement a setter method for each field set that way. -WARNING: Call the setter, as above - do not assign to the field as if it were a property. Nashorn rewrote `bean.text = value` into `bean.setText(value)`; on GraalJS the assignment fails with `Unknown identifier`. See <>. +[WARNING] +==== +The setter must be called. Assigning to the field as if it were a property is supported by Nashorn only, which rewrote such an assignment into the corresponding setter call; on GraalJS it fails with an `Unknown identifier` error. See <>. + +[source,typescript] +---- +bean.setText('value'); // supported on both engines +bean.text = 'value'; // Nashorn only; fails on GraalJS +---- +==== The Java class used in the example above looks like this: @@ -118,14 +127,27 @@ There are some type conversions that are made when calling from JavaScript to Ja - when passing a JavaScript `array`, the Java method should expect a Java `List` - when passing a JavaScript `object`, the Java method should expect a Java `Map` -The argument must already be of the type the method declares - it is not coerced on the way in. A JavaScript string passed to a Java `int` or `Long` parameter throws on GraalJS. For instance, HTTP request parameters always arrive as strings, so parse them before the call. Wrap objects and arrays in `+__.toScriptValue()+` to hand them over as a Java `Map` or `List`. +The argument must already be of the type the method declares. Nashorn coerced it where the two differed; GraalJS does not. HTTP request parameters, for instance, always arrive as strings and must be parsed before they are passed to a parameter declared as a number. Objects and arrays are handed over as a Java map or list by converting them first. + +[source,typescript] +---- +bean.setSize(parseInt(req.params.size, 10)); // a Java Long +bean.setConfig(__.toScriptValue(params.config)); // a Java Map +---- === Returning results from Java When returning simple values from Java to a JavaScript caller, the same type conversions applies. -Complex values are *not* converted: a Java `Map` returned to JavaScript is still a Java object, and inspecting it with `Object.keys()`, spread or `JSON.stringify()` reports the class methods rather than the entries. Convert it once, in the module that makes the call, with `+__.toNativeObject()+`, as the `readLines` example above does. See <>. +Complex values are not converted. A Java map returned to JavaScript remains a Java object, and inspecting it with the standard JavaScript operations reports the methods of its class rather than its entries. It is converted once, in the module that makes the call, as the `readLines` example above does. + +[source,typescript] +---- +const config = __.toNativeObject(bean.getConfig()); +---- + +See <>. To return complex object values, you should create a specific Java class to make the mapping. This class should implement the ``MapSerializable`` interface. It will implement the ``serialize`` method, which allows generating a JSON object. From 0632081d064fd1fda9a06c70eb9683dd0ba628bd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 20:11:27 +0000 Subject: [PATCH 4/5] Clarify what fails on a bean property assignment Review feedback on the wording. The assignment is to a property on the bean object, not to a Java field, so say that GraalJS exposes the bean's methods and public fields and no property named after the setter. Also state plainly that inspecting an unconverted Java map throws nothing, rather than that it "raises nothing". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dx8tfKuJaZd2rDvkF8Aoba --- docs/runtime/engines.adoc | 4 ++-- docs/runtime/java-bridge.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/runtime/engines.adoc b/docs/runtime/engines.adoc index a5a2952a..1ca3690d 100644 --- a/docs/runtime/engines.adoc +++ b/docs/runtime/engines.adoc @@ -77,7 +77,7 @@ bean.setConfig(__.toScriptValue(params.config)); const result = __.toNativeObject(bean.execute()); ---- -A Java map returned without conversion remains a Java object. Inspecting it with the standard JavaScript operations reports the methods of its class instead of its entries, and reports no error in doing so. An individual property read may still return the expected value, which is what makes the omission easy to overlook. +A Java map returned without conversion remains a Java object. Inspecting it with the standard JavaScript operations returns the methods of its class instead of its entries, and throws nothing along the way. An individual property read may still return the expected value, which is what makes the omission easy to overlook. [source,typescript] ---- @@ -95,7 +95,7 @@ Java lists are the exception. They present array semantics on both engines, so i ==== Call setters rather than assigning properties -Nashorn rewrote an assignment to a bean property into a call to the corresponding setter. GraalJS does not, and the assignment fails with an `Unknown identifier` error. +Nashorn accepted an assignment to a property named after the setter and rewrote it into a call to that setter. GraalJS exposes the bean's methods and its public fields, and nothing else, so no such property exists and the assignment fails with an `Unknown identifier` error. [source,typescript] ---- diff --git a/docs/runtime/java-bridge.adoc b/docs/runtime/java-bridge.adoc index ae48eef4..48a2859e 100644 --- a/docs/runtime/java-bridge.adoc +++ b/docs/runtime/java-bridge.adoc @@ -75,11 +75,11 @@ exports.doSomethingElse = function (params) { NOTE: When passing values that might be `null` or `undefined` it is recommended to filter them using the `__.nullOrValue` built-in function. This function converts any value that is `null` or `undefined` in JavaScript to `null` in Java. Otherwise returns the input value without changes. -The Java object must implement a setter method for each field set that way. +The Java object must implement a setter method for each parameter passed that way. [WARNING] ==== -The setter must be called. Assigning to the field as if it were a property is supported by Nashorn only, which rewrote such an assignment into the corresponding setter call; on GraalJS it fails with an `Unknown identifier` error. See <>. +The setter must be called. Nashorn also accepted an assignment to a property named after the setter and rewrote it into a call to that setter, but GraalJS exposes no such property, and the assignment fails with an `Unknown identifier` error. See <>. [source,typescript] ---- From fb0a43ae92e30cb9399c0b104112e7cbbc2a142d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 20:14:51 +0000 Subject: [PATCH 5/5] Drop the aside about how the libraries are written Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dx8tfKuJaZd2rDvkF8Aoba --- docs/runtime/engines.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/runtime/engines.adoc b/docs/runtime/engines.adoc index 1ca3690d..eaae6e22 100644 --- a/docs/runtime/engines.adoc +++ b/docs/runtime/engines.adoc @@ -66,7 +66,7 @@ The <> is identical on both engines: the same bridge ==== Convert values at the boundary -Values are converted explicitly: on the way into Java with `toScriptValue`, and on the way back out with `toNativeObject`. This is how the platform <<../libraries#, libraries>> are written. +Values are converted explicitly: on the way into Java with `toScriptValue`, and on the way back out with `toNativeObject`. [source,typescript] ----