How can I pass a proper method reference in so Nas

2019-04-14 05:49发布

问题:

I am trying to write a library that will let me execute JSON Logic rules via the Nashorn Javascript engine.

My problem right now is specifically around the JSObject wrapper I've created to handle moving data from Java/Kotlin into the scripting engine.

If an array is passed in such as [true] it is wrapped and the json-logic script will receive it, see that it is an array, and attempt to run the following bit of code:

if(Array.isArray(logic)) {
  return logic.map(function(l) {
    return jsonLogic.apply(l, data);
  });
}

When the .map function is called, Nashorn will call getMember("map") on my object expecting to get a function back that it can execute.

This is where I am stuck. I have not been able to find any sort of proper syntax to give Nashorn a method or method reference that can be invoked by it as the receiver of its map function.

The code is available here: https://github.com/deinspanjer/json-logic-java There are some basic unit tests including the one that exhibits the problem, JavaJsonLogicTest.simpleApplyJEJO(). The line of code that is broken is com/jsonlogic/JSObjectWrappers.kt:97.

I would very much appreciate your help.

UPDATE: Based on the accepted answer, here is the working Kotlin version of the code:

package com.jsonlogic

import jdk.nashorn.api.scripting.AbstractJSObject
import jdk.nashorn.api.scripting.JSObject
import java.util.function.Function
import javax.script.ScriptEngineManager

fun main(args: Array<String>) {
    val m = ScriptEngineManager()
    val e = m.getEngineByName("nashorn")

    // The following JSObject wraps this list
    val l = mutableListOf<Any>()
    l.add("hello")
    l.add("world")
    l.add(true)
    l.add(1)

    val jsObj = object : AbstractJSObject() {
        override fun getMember(name: String?): Any? {
            if (name == "map") {
                // return a functional interface object - nashorn will treat it like
                // script function!
                return Function { callback: JSObject ->
                    val res = l.map {
                        // call callback on each object and add the result to new list
                        callback.call(null, it)
                    }

                    // return fresh list as result of map (or this could be another wrapper)
                    res
                }
            } else {
                // unknown property
                return null
            }
        }
    }

    e.put("obj", jsObj)
    // map each String to it's uppercase and print result of map
    e.eval("print(obj.map(function(x) '\"'+x.toString()+'\"'))");
}

回答1:

JSObject.getMember can return any script "callable". That could be another JSObject that returns 'true' for isFunction or a Java functional interface object. Couple of simple Java sample programs here:

import javax.script.*;
import jdk.nashorn.api.scripting.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager m = new ScriptEngineManager();
        ScriptEngine e = m.getEngineByName("nashorn");

        // The following JSObject wraps this list
        List<String> l = new ArrayList();
        l.add("hello");
        l.add("world");

        JSObject jsObj = new AbstractJSObject() {
            @Override
            public Object getMember(String name) {
                // return a "function" object for "map"
                if (name.equals("map")) {
                    return new AbstractJSObject() {
                        @Override
                        public Object call(Object thiz, Object... args) {
                            // first argument is the callback passed from script
                            JSObject callable = (JSObject)args[0];
                            List<Object> res = new ArrayList<>();
                            for (Object obj : l) {
                                // call callback on each object and add the result to new list
                                res.add(callable.call(null, obj));
                            }

                            // return fresh list as result of map (or this could be another wrapper)
                            return res;
                        }

                        @Override
                        public boolean isFunction() { return true; }
                    };
                } else {
                    // unknown property
                    return null;
                }
           }
        };

        e.put("obj", jsObj);
        // map each String to it's uppercase and print result of map
        e.eval("print(obj.map(function(x) x.toUpperCase()))");
    }
}

The above example returns a callable JSObject for "map" property. The returned "function" itself uses a callback function as argument. All script functions (and objects) are passed as JSObjects to Java code and so 'map' code casts first argument to JSObject to invoke the script callback function.

The above sample modified to use a functional interface is as follows:

import javax.script.*;
import jdk.nashorn.api.scripting.*;
import java.util.*;
import java.util.function.*;

public class Main2 {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager m = new ScriptEngineManager();
        ScriptEngine e = m.getEngineByName("nashorn");

        // The following JSObject wraps this list
        List<String> l = new ArrayList();
        l.add("hello");
        l.add("world");

        JSObject jsObj = new AbstractJSObject() {
            @Override
            public Object getMember(String name) {
                if (name.equals("map")) {
                    // return a functional interface object - nashorn will treat it like
                    // script function!
                    return (Function<JSObject, Object>)callback -> {
                        List<Object> res = new ArrayList<>();
                        for (Object obj : l) {
                            // call callback on each object and add the result to new list
                            res.add(callback.call(null, obj));
                        }

                        // return fresh list as result of map (or this could be another wrapper)
                        return res;
                    };
                } else {
                    // unknown property
                    return null;
                }
           }
        };

        e.put("obj", jsObj);
        // map each String to it's uppercase and print result of map
        e.eval("print(obj.map(function(x) x.toUpperCase()))");
    }
}

Hope the above samples will help you in coming up with Kotlin version for your scenario.