Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Capturing Nashorn's Global Variables

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??

like image 727
AJNeufeld Avatar asked Mar 04 '16 23:03

AJNeufeld


People also ask

How do I find all global variables?

Use the env command. The env command returns a list of all global variables that have been defined. If a global variable exists in a script that hasn't been run yet, it will not show up in the output from env .

What is the potential problem with globally scoped variables?

The problem with global variables is that since every function has access to these, it becomes increasingly hard to figure out which functions actually read and write these variables. To understand how the application works, you pretty much have to take into account every function which modifies the global state.

How do you manage global variables?

To modify the details of a global variable, click the Global variable name in the table and edit the values in the Properties section. . You can copy a global variable and save it with a different name to create a new global variable.

Can we override global variables?

You can override global variables to pass data from the GUI to the application. Local and shared global variables can be overridden. For more information, see Global variable overrides.


1 Answers

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.

like image 69
AJNeufeld Avatar answered Sep 23 '22 13:09

AJNeufeld