Capturing Nashorn's Global Variables
Asked Answered
V

1

7

I have a Java 7 program, that loads thousands of objects (components), each with many parameters (stored in a Map), and executes various Rhino scripts on those objects to calculate other derived parameters which get stored back in the object's Map. Before each script is run, a Scope object is created, backed by the object's map, which is used as a JavaScript's scope for the duration of the script.

As a simple example, the following creates a HashMap with a=10 and b=20, and executes the script c = a + b, which results in c = 30.0 stored back in the map. Although the script looks like it is creating a global variable c, the Scope object captures it and stores it in the map; another script executed with a different Scope object won't see this variable:

public class Rhino {

    public static void main(String[] args) throws ScriptException {
        Context cx = Context.enter();
        Scriptable root_scope = cx.initStandardObjects();

        Map<String, Object> map = new HashMap<>();
        map.put("a", 10);
        map.put("b", 20);

        Scope scope = new Scope(root_scope, map);
        cx.evaluateString(scope, "c = a + b", "<expr>", 0, null);
        System.out.println(map); // --> {b=20, c=30.0, a=10}

        Context.exit();
    }

    static class Scope extends ScriptableObject {

        private Map<String, Object> map;

        public Scope(Scriptable parent, Map<String, Object> map) {
            setParentScope(parent);
            this.map = map;
        }

        @Override
        public boolean has(String key, Scriptable start) {
            return true;
        }

        @Override
        public Object get(String key, Scriptable start) {
            if (map.containsKey(key))
                return map.get(key);
            return Scriptable.NOT_FOUND;
        }

        @Override
        public void put(String key, Scriptable start, Object value) {
            map.put(key, value);
        }

        @Override
        public String getClassName() {
            return "MapScope";
        }
    }
}

The above script outputs {b=20, c=30.0, a=10}, showing the variable c has been stored in the Map.

Now, I need to migrate this the Java 8, and use Nashorn. However, I am finding that Nashorn always stores global variables in a special "nashorn.global" object. In fact, it seems to be treating all bindings as read-only, and attempts to change an existing variable instead results in a new global variable shadowing the existing binding.

public class Nashorn {

    private final static ScriptEngineManager MANAGER = new ScriptEngineManager();

    public static void main(String[] args) throws ScriptException {
        new Nashorn().testBindingsAsArgument();
        new Nashorn().testScopeBindings("ENGINE_SCOPE", ScriptContext.ENGINE_SCOPE);
        new Nashorn().testScopeBindings("GLOBAL_SCOPE", ScriptContext.GLOBAL_SCOPE);
    }

    private ScriptEngine engine = MANAGER.getEngineByName("nashorn");
    private Map<String, Object> map = new HashMap<>();
    private Bindings bindings = new SimpleBindings(map);

    private Nashorn() {
        map.put("a", 10);
        map.put("b", 20);
    }

    private void testBindingsAsArgument() throws ScriptException {
        System.out.println("Bindings as argument:");
        engine.eval("c = a + b; a += b", bindings);
        System.out.println("map = " + map);
        System.out.println("eval('c', bindings) = " + engine.eval("c", bindings));
        System.out.println("eval('a', bindings) = " + engine.eval("a", bindings));
    }

    private void testScopeBindings(String scope_name, int scope) throws ScriptException {
        System.out.println("\n" + scope_name + ":");
        engine.getContext().setBindings(bindings, scope);
        engine.eval("c = a + b; a += b");
        System.out.println("map = " + map);
        System.out.println("eval('c') = " + engine.eval("c"));
        System.out.println("eval('a') = " + engine.eval("a"));
    }
}

Output:

Bindings as argument:
map = {a=10, b=20, nashorn.global=[object global]}
eval('c', bindings) = 30.0
eval('a', bindings) = 30.0

ENGINE_SCOPE:
map = {a=10, b=20, nashorn.global=[object global]}
eval('c') = 30.0
eval('a') = 30.0

GLOBAL_SCOPE:
map = {a=10, b=20}
eval('c') = 30.0
eval('a') = 30.0

The eval output lines show the results are correctly computed and are being stored, but the map output lines shows the results are not being stored where I desire them to be.

This is not acceptable, for a variety of reasons. The individual objects do not get the computed parameters stored back in their own local storage. Variables from other scripts executing on other objects will carry over from previous script executions, which could hide logic errors (a script could accidentally use an undefined variable name, but if it that name was actually used by a previous script, the old garbage value could be used instead of a ReferenceError being generated, hiding the error).

Following the engine.eval() with map.put("c", engine.get("c")) would move the result to where I need it to be, but with an arbitrary script, I do not know what all the variable names would be, so is not an option.

So the question: is there anyway to capture the creation of global variables, and store them instead inside a Java object under control of the application, such as the original Binding object??

Vibraphone answered 4/3, 2016 at 23:5 Comment(0)
V
6

I have a solution that seems to work, but it clearly is a hack.

Test program:

public class Nashorn {
    public static void main(String[] args) throws ScriptException {
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");

        Map<String, Object> map = new HashMap<>();
        map.put("a", 10);
        map.put("b", 20);

        try (GlobalMap globals = new GlobalMap(map)) {
            engine.eval("c = a + b; a += b;", globals);
        }

        System.out.println("map = " + map);
    }
}

The test program outputs map = {a=30.0, b=20, c=30.0} as desired.

The GlobalMap intercepts the storing of the Nashorn global object under the key "nashorn.global", so it doesn't get stored in the map. When the GlobalMap is closed, it removes any new global variables from the Nashorn global object and stores them in the original map:

public class GlobalMap extends SimpleBindings implements Closeable {

    private final static String NASHORN_GLOBAL = "nashorn.global";
    private Bindings global;
    private Set<String> original_keys;

    public GlobalMap(Map<String, Object> map) {
        super(map);
    }

    @Override
    public Object put(String key, Object value) {
        if (key.equals(NASHORN_GLOBAL) && value instanceof Bindings) {
            global = (Bindings) value;
            original_keys = new HashSet<>(global.keySet());
            return null;
        }
        return super.put(key, value);
    }

    @Override
    public Object get(Object key) {
        return key.equals(NASHORN_GLOBAL) ? global : super.get(key);
    }

    @Override
    public void close() {
        if (global != null) {
            Set<String> keys = new HashSet<>(global.keySet());
            keys.removeAll(original_keys);
            for (String key : keys)
                put(key, global.remove(key));
        }
    }
}

I am still hoping to find a solution where the current scope could be set to a Map<String,Object> or Bindings object, and any new variables created by the script are stored directly in that object.

Vibraphone answered 7/3, 2016 at 22:22 Comment(3)
It's so stupid we have to resort to this, the nashorn engine is as stubborn as its creators i bet...Casuistry
Invoking the expression inside an IIFE does the trick as well (you need to use var keyword for declaring variables). Anyone knows if that method has any caveats?Wallasey
@Wallasey Thanks for the suggestion. Using an IIFE with var c = ... does keeps c from being stored in the global scope. This does help isolate the code, preventing different scripts from accidentally seeing values computed in other scripts. But this doesn't help with the original problem: actually capturing the computed values stored to these variables in the Map provided by the Java program executing these JavaScript code snippits, for use later in other parts of the program.Vibraphone

© 2022 - 2024 — McMap. All rights reserved.