Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Seamlessly pass Arrays and Lists to and from Nashorn

I know you can work with Java arrays in Nashorn and there are plenty of examples of how to do this. The problem for me with the standard approach is that it makes the javascript code explicitly aware of it's runtime environment. Currently I have a solution that makes use of Rhino and it seamlessly converts between Java type and Native javascript types.

For Rhino I accomplished this by implementing org.mozilla.javascript.ContextFactory and org.mozilla.javascript.WrapFActory and setting WrapFactory on the Context when makeContext is called. This WrapFactory implementation takes care of converting between Java arrays and Lists and Native javascript arrays and lists. It's also wroth mentioning that I had to get the Rhino source code from the JDK to get this approach to work.

I need to find a similar solution for Nashorn. Here is an example of what I am trying to accomplish.

public static void main(String args[]) {
    NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
    ScriptEngine engine = factory.getScriptEngine();
    try {
        engine.eval("function print_array(arr) { print(arr); }");
        engine.eval("function print_native() { print_array([1, 2, 3, 4]); }");
        Invocable invocable = (Invocable) engine;
        invocable.invokeFunction("print_array", new int[]{1, 2, 3, 4});
        invocable.invokeFunction("print_array", Arrays.asList(1, 2, 3, 4));
        invocable.invokeFunction("print_native");
    } catch (ScriptException | NoSuchMethodException e) {
        e.printStackTrace();
    }
}

The output of this code is

[I@169e6180

[1, 2, 3, 4]

1,2,3,4

I am looking for a way to implement a ScriptObjectMirror, assuming that is even correct, that would make the output of those three invokeFunction calls be the same.

I have tried using wrap function on ScriptUtils, but still the result is wrong

UPDATE

I tried to create a dynamic proxy of type Invocable and do conversions in the InvocationHandler. To create a NativeArray with Nashorn it seems you should use jdk.nashorn.internal.objects.Global.allocate, but this always raises an exception.

Global.allocate(new int[] {1, 2, 3, 4})

Raises

Exception in thread "main" java.lang.NullPointerException
    at jdk.nashorn.internal.objects.Global.instance(Global.java:491)
    at jdk.nashorn.internal.objects.NativeArray.<init>(NativeArray.java:141)
    at jdk.nashorn.internal.objects.Global.allocate(Global.java:1584)
like image 908
Leon Avatar asked Jun 01 '15 10:06

Leon


1 Answers

I think you have to go the hard road down an implement a AbstractJSObject. I think a lot of Functions like getMember can be done via Refelction. But what would you do is somebody thinking it is a JS Array and try to extend the Prototype? Do you want to handle this too? In that case I would implement a JS Array as property in a list like wrapper class and delegate all set/add to a JS function updating the JS object.

Solution 1:

public static void main(String args[]) {
        NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
        ScriptEngine engine = factory.getScriptEngine();

        try {
            engine.eval("function print_array(arr) { print(arr); for(var i=0; i<arr.length; i++) {print(arr[i]);}}");
            engine.eval("function print_native() { print_array([1, 2, 3, 4]); }");
            engine.eval("function get_native() { return [1, 2, 3, 4]; }");
            Invocable invocable = (Invocable) engine;
            invocable.invokeFunction("print_array", new int[]{1, 2, 3, 4});
            invocable.invokeFunction("print_array", Arrays.asList(1, 2, 3, 4));
            invocable.invokeFunction("print_array", new Foo());
            invocable.invokeFunction("print_native");

            ScriptObjectMirror a = (ScriptObjectMirror) invocable.invokeFunction("get_native");
            System.out.println(invocable.invokeFunction("get_native"));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class Foo extends AbstractJSObject {
        Map<Integer, Object> arrayValues = new HashMap<>();

        public Foo() {
            arrayValues.put(0, 1);
            arrayValues.put(1, 2);
            arrayValues.put(2, 3);
        }
        @Override
        public Object call(Object thiz, Object... args) {
            System.out.println("call");
            return super.call(thiz, args);
        }

        @Override
        public Object newObject(Object... args) {
            System.out.println("new Object");
            return super.newObject(args);
        }

        @Override
        public Object eval(String s) {
            System.out.println("eval");
            return super.eval(s);
        }

        @Override
        public Object getMember(String name) {
            System.out.println("getMember " + name);
            return name.equals("length") ? arrayValues.size() : arrayValues.get(Integer.valueOf(name));
        }

        @Override
        public Object getSlot(int index) {
            //System.out.println("getSlot");
            return arrayValues.get(index);
        }

        @Override
        public boolean hasMember(String name) {
            System.out.println("hasMember");
            return super.hasMember(name);
        }

        @Override
        public boolean hasSlot(int slot) {
            System.out.println("hasSlot");
            return super.hasSlot(slot);
        }

        @Override
        public void removeMember(String name) {
            System.out.println("removeMember");
            super.removeMember(name);
        }

        @Override
        public void setMember(String name, Object value) {
            System.out.println("setMember");
            super.setMember(name, value);
        }

        @Override
        public void setSlot(int index, Object value) {
            System.out.println("setSlot");
            super.setSlot(index, value);
        }

        @Override
        public Set<String> keySet() {
            System.out.println("keySet");
            return arrayValues.keySet().stream().map(k -> "" + k).collect(Collectors.toSet());
        }

        @Override
        public Collection<Object> values() {
            System.out.println("values");
            return arrayValues.values();
        }

        @Override
        public boolean isInstance(Object instance) {
            System.out.println("isInstance");
            return super.isInstance(instance);
        }

        @Override
        public boolean isInstanceOf(Object clazz) {
            System.out.println("isINstanceOf");
            return super.isInstanceOf(clazz);
        }

        @Override
        public String getClassName() {
            System.out.println("getClassName");
            return super.getClassName();
        }

        @Override
        public boolean isFunction() {
            return false;
        }

        @Override
        public boolean isStrictFunction() {
            return false;
        }

        @Override
        public double toNumber() {
            return super.toNumber();
        }

        @Override
        public boolean isArray() {
            return true;
        }

        @Override
        public String toString() {
            return arrayValues.values().toString();
        }
    }

Solution 2 would be (in pseudo code):

static class FooList implements List {
        final ScriptObjectMirror wrapped;

        public FooList(ScriptObjectMirror wrapped) {
            this.wrapped = wrapped;
        }

        @Override
        public int size() {
            return engine.eval("get length of wrapped JS object");
        }

        ... and so on ...
    }
like image 92
KIC Avatar answered Sep 28 '22 09:09

KIC