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??
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 .
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With