Running your JS code in Java

tl;dr java-xls 

After the last post on running JS code in python, quite a few people asked me about running JS code in Java.  I guess it’s not too surprising, given that working with Rhino is like pulling teeth.  But here goes:

Note that I use vim, not Eclipse or some other IDE, so some steps may seem unnatural.  However, I felt that it was truer to form to keep it all in a Makefile.

The standard choice for Java is Rhino, from the folks at Mozilla.  I wish they kept good documentation, but MDN is having issues: their API javadoc Reference link points to the main page and the fallback is also broken.  I recommend referencing the source directly.  

The first step is to get the rhino JAR.  This step is fairly straight-forward:

$ git clone https://github.com/mozilla/rhino
$ cd rhino
$ ant jar
$ cd -
$ cp rhino/build/rhino*/js.jar rhino.jar

All of the relevant rhino classes are in the package org.mozilla.javascript.

The first class T will evaluate 1+1 and print the output:

import org.mozilla.javascript.*;
public class T {
public static void main(String[] args) throws Exception { Context cx = Context.enter();
Scriptable scope = cx.initStandardObjects();
String script = "1+1";
String out = cx.evaluateString(scope, script, "<cmd>", 1, null);
System.out.println(out);
}
}

Save this to T.java and compile:

$ javac -cp .:rhino.jar T.java && java -cp .:rhino.jar T
2

Variables are available in the same fashion:

String script = "var x = 1+1";
cx.evaluateString(scope, script, "<cmd>", 1, null);
script = "Math.pow(x,x)";
String out = cx.evaluateString(scope, script, "<cmd>", 1, null);

The other way to access variables is to use an accessor:

String out = scope.get("x", scope).toString();

Unfortunately, to request something deeper like “foo.bar.baz.qux”, you can’t just request it by path:

String script = "var foo = {bar: {baz: {qux:1}}}";
String out = scope.get("foo.bar.baz.qux", scope).toString();
(error) org.mozilla.javascript.UniqueTag@442a15cd: NOT_FOUND

The correct approach is to recursively apply.

String script = "var foo = {bar: {baz: {qux:'dafuq'}}}";
String out = (String)(((NativeObject)((NativeObject)((NativeObject)scope.get("foo", scope)).get("bar",scope)).get("baz",scope)).get("qux",scope));

so I wrote a helper function to do the right thing for “foo.bar.baz.qux”.

Like with PyV8, rhino lacks console, and the error looks something like:

Exception in thread "main" org.mozilla.javascript.EcmaError: ReferenceError: "console" is not defined. (<cmd>#1)
at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3689)
at org.mozilla.javascript.ScriptRuntime.constructError(ScriptRuntime.java:3667)
at org.mozilla.javascript.ScriptRuntime.notFoundError(ScriptRuntime.java:3752)
at org.mozilla.javascript.ScriptRuntime.name(ScriptRuntime.java:1727)
at org.mozilla.javascript.gen._cmd__1._c_script_0(<cmd>:1)
at org.mozilla.javascript.gen._cmd__1.call(<cmd>)
at org.mozilla.javascript.ContextFactory.doTopCall(ContextFactory.java:394)
at org.mozilla.javascript.ScriptRuntime.doTopCall(ScriptRuntime.java:3090)
at org.mozilla.javascript.gen._cmd__1.call(<cmd>)
at org.mozilla.javascript.gen._cmd__1.exec(<cmd>)
at org.mozilla.javascript.Context.evaluateString(Context.java:1079)
at T.main(T.java:13)

The workaround is to call `java.lang.System.out.println` (what a mouthful)

Unfortunately the NativeArray types are difficult to deal with, so a manual translation to a Java array is the easiest approach

String[] out = new String[(int)native_array.getLength()];
int idx;
for(Object o :native_array.getIds()) out[idx = (Integer)o] = native_array.get(idx, native_array).toString();

There are other helpers to convert between Java and JS, none of which are as pleasant as PyV8

In summary, the experience left much to be desired.  3/10, would advise others to re-evaluate decision to use Rhino in the first place, but if you have to do it I recommend just modifying the java-xls template (and the com.sheetjs.JSHelper class has a few helper functions)

  1. sheetjs posted this