FXMLLoader: not supplying filename to script engine, not supplying event object as argument to script

Rony G. Flatscher Rony.Flatscher at wu.ac.at
Wed Nov 13 14:14:59 UTC 2019

Hmm, not getting any feedback so far, so wondering if there are currently any Java developers who
take advantage of the ability of FXMLLoader to have FXML controllers implemented in any of the Java
javax.script languages?

For those, who use scripting languages for FXML controllers the request that FXMLLoader adds both
entries, ScriptEngine.FILENAME (for debugging, logging) and ScriptEngine.ARGV () (for making the
event object available directly as an argument) into the engine Bindings, should be quite helpful
while developing and running the scripts.

[Personally I am using the scripting engine ooRexx successfully for teaching oo programming from
scratch to JavaFX in a single semester (four hour lecture, eight ECTS) at a Business Administration
university. So these two missing features in the current FXMLLoader support for FXML controllers
would help tremendously, especially in case of coding errors as currently it is not clear from which
file the script that has an error comes from, making it extremely cumbersome and time consuming in
JavaFX applications that use multiple and complex FXML files.]

Therefore I would kindly ask interested committers for mentoring the proposed changes. Enclosed
please find a simpler version of the patch that adds these missing features to the ENGINE_SCOPE
Bindings in the three locations where ScriptEngine.eval() gets invoked (it ).

To comment this simple patch, maybe I should add a few remarks such that the context becomes clear:

  * invoking a script via ScriptEngine.eval() will always be accompanied with a ScriptContext that
    usually maintains two Bindings (Maps):

      o one, GLOBAL_SCOPE Bindings, for global entries (used e.g. for putting the FXML elements that
        have an fx:id attribute defined, such that scripts can get access to them, independent of a
        particular ScriptEngine) which can also be used for sharing values among different script

      o one, ENGINE_SCOPE Bindings, usually used for individual invocations.

  * while a FXML file gets processed sequentially by the FXMLLoader elements in the form of
    "<fx:script source="someScript.ext" />" will cause invoking the ScriptEngine.eval(Reader): this
    patch fetches the ENGINE_SCOPE Bindings and puts the value "someScript.ext" with the key
    ScriptEngine.FILENAME into it (cf. "@@ -1558,6 +1558,9 @@ public class FXMLLoader" and "@@
    -1582,6 +1585,8 @@ public class FXMLLoader" in the patch),

  * if an event handler gets defined (e.g. with the event attribute "<fx:button ...
    onAction="someScript">") the FXMLLoader creates a ScriptEventHandler and stores "someScript" and
    the ScriptEngine for executing that script whenever the event fires.

      o When an event fires, the current implementation creates a copy of the current ENGINE_SCOPE
        Bindings from the ScriptEngine's ScriptContext, adds its entries to it after saving the
        entry "event" with the ActionEvent object in it. It then changes the ScriptEngine's current
        ScriptContext such that it now uses the new copy of the Bindings as its ENGINE_SCOPE
        Bindings, runs the script using eval() and then restores the ScriptContext ENGINE_SCOPE

      o The supplied patch (cf. "@@ -1675,30 +1680,28 @@ public class FXMLLoader") instead will
        create a copy of the ENGINE_SCOPE Bindings only once at creation time (and puts the
        appropriate ScriptEngine.FILENAME into it using the name of the FXML file that defines the
        event script attribute) and will reuse that Bindings each time the handler gets invoked,
        after putting the actual "event" object and the respective ScriptEngine.ARGV entry into it.
        Using ScriptEngine.eval(String,Bindings) will use the supplied Bindings as the ENGINE_SCOPE
        Bindings for this invocation only, such that no restore is necessary upon return.

As only entries get added to the engine Bindings that have not been used by FXMLLoader this simple
patch should not affect existing scripts. The patch has been tested and works.

Maybe it helps the cause for applying this patch, if I point out that I have been active in a number
of opensource projects, including Apache's BSF which led to my participation as an expert in JSR-223
which originally defined the javax.script framework introduced with Java 6 (also authored a complete
ScriptEngine implementation with both, the javax.script.Compilable and the javax.script.Invocable

So looking for interested committers who would be willing to mentor this patch. Please advise.


On 06.11.2019 16:05, Rony G. Flatscher wrote:
> Using a script engine (javax.script.ScriptEngine) for implementing a FXML controller there are two
> important information missing in the ScriptContext.ENGINE_SCOPE Bindings supplied to the script used
> to eval() the script code:
>   * ScriptEngine.FILENAME
>       o This value denotes the file name from where the script code was fetched that is being eval()'d.
>       o When debugging script controllers in a complex JavaFX application it is mandatory to know
>         the file name the script code was taken from (as such scripts could be called/run from
>         different FXML files). Also, in the case of script runtime errors, usually the file name is
>         given by the script engine where the error has occurred to ease debugging, such that it is
>         important to really supply the filename.
>           + Note: the 'location'-URL in ScriptContext.GLOBAL_SCOPE refers the FXML file,  not to the
>             file that hosts the script that gets run if using the "<fx:script" element where the
>             "source" attribute denotes the name of the script file.
>       o General solution: supply the appropriate ScriptEngine.FILENAME entry to the
>         ScriptContext.ENGINE_SCOPE Bindings.
>   * ScriptEngine.ARGV
>       o This value denotes the arguments that get passed to the script from Java in form of a Java
>         Array of type Object.
>       o When defining event handlers in FXML files in script code the script does not get the
>         appropriate argument. Rather the script programmer needs to access the
>         ScriptContext.ENGINE_SCOPE and fetch the entry named "event" from there. Some script engines
>         may make the entries in the Bindings implicitly available to the scripts, however this
>         cannot be expected by default. However, a ScriptEngine.ARGV entry must be supplied to the
>         script by the script engine implementor, such that a script coder gets the event object
>         argument in the script language's manner.
>       o General solution: supply the appropriate ScriptEngine.ARGV Object array to the
>         ScriptContext.ENGINE_SCOPE Bindings.
> With these two changes not only writing controller scripts would be eased, it also would
> instrumentate ScriptContext.ENGINE_SCOPE Bindings the way it was intended by JSR-223.
> Enclosed please find a tested diff for FXMLLoader.java from the current OpenJavaFX Master (version
> 14) that implements both, ScriptEngine.FILENAME entries for all script invocations and in the case
> of a script event handler the appropriate ScriptEngine.ARGV entry gets supplied, allowing the script
> to fetch the event object directly as an argument.
> As I have signed the OCA the code (in form of a git diff) can be directly applied to FXMLLoader.java.
> If you need the patch in a different form, then please advise.
> ---rony

-------------- next part --------------
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/modules/javafx.fxml/src/main/java/javafx/fxml/FXMLLoader.java b/modules/javafx.fxml/src/main/java/javafx/fxml/FXMLLoader.java
index 7f3d2f3083..f9e2400f4c 100644
--- a/modules/javafx.fxml/src/main/java/javafx/fxml/FXMLLoader.java
+++ b/modules/javafx.fxml/src/main/java/javafx/fxml/FXMLLoader.java
@@ -1558,6 +1558,9 @@ public class FXMLLoader {
                         location = new URL(FXMLLoader.this.location, source);
+                    Bindings engineBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
+                    engineBindings.put(engine.FILENAME, location.getPath());
                     InputStreamReader scriptReader = null;
                     try {
                         scriptReader = new InputStreamReader(location.openStream(), charset);
@@ -1582,6 +1585,8 @@ public class FXMLLoader {
             if (value != null && !staticLoad) {
                 // Evaluate the script
                 try {
+                    Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
+                    engineBindings.put(scriptEngine.FILENAME, location.getPath());
                 } catch (ScriptException exception) {
@@ -1675,30 +1680,28 @@ public class FXMLLoader {
     private static class ScriptEventHandler implements EventHandler<Event> {
         public final String script;
         public final ScriptEngine scriptEngine;
+        public final Bindings engineBindings;
         public ScriptEventHandler(String script, ScriptEngine scriptEngine) {
             this.script = script;
             this.scriptEngine = scriptEngine;
+            this.engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
+            URL location=(URL) scriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE).get(LOCATION_KEY);
+            this.engineBindings.put(scriptEngine.FILENAME, location.getPath());
         public void handle(Event event) {
-            // Don't pollute the page namespace with values defined in the script
-            Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
-            Bindings localBindings = scriptEngine.createBindings();
-            localBindings.put(EVENT_KEY, event);
-            localBindings.putAll(engineBindings);
-            scriptEngine.setBindings(localBindings, ScriptContext.ENGINE_SCOPE);
+            // Don't pollute the page namespace with values defined in the script use initial engineBindings
+            engineBindings.put(EVENT_KEY, event);
+            engineBindings.put(scriptEngine.ARGV, new Object[]{event});
             // Execute the script
             try {
-                scriptEngine.eval(script);
+                scriptEngine.eval(script,engineBindings);
             } catch (ScriptException exception){
                 throw new RuntimeException(exception);
-            // Restore the original bindings
-            scriptEngine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);

More information about the openjfx-dev mailing list