Search code examples
javascriptengine

How to store function handles from ScriptManager for later usage?


tl;dr:
How do/can I store the function-handles of multiple js-functions in java for using them later? Currently I have two ideas:

  1. Create multipe ScriptEngine instances, each containing one loaded function. Store them in a map by column, multiple entries per column in a list. Looks like a big overhead depending on how 'heavy' a ScriptEngine instance is...

  2. Some Javascript solution to append methods of the same target field to an array. Dont know yet how to access that from the java-side, but also dont like it. Would like to keep the script files as stupid as possible.

     var test1 = test1 || [];
     test1.push(function(input) { return ""; });
    
  3. ???

Ideas or suggestions?


Tell me more:
I have a project where I have a directory containing script files (javascript, expecting more than hundred files, will grow in future). Those script files are named like: test1;toupper.js, test1;trim.js and test2;capitalize.js. The name before the semicolon is the column/field that the script will be process and the part after the semicolon is a human readable description what the file does (simplified example). So in this example there are two scripts that will be assigned to the "test1" column and one script to the "test2" column. The js-function template basically looks like:

    function process(input) { return ""; };

My idea is, to load (and evaluate/compile) all script files at server-startup and then use the loaded functions by column when they are needed. So far, so good.

I can load/evaluate a single function with the following code. Example uses GraalVM, but should be reproducable with other languages too.

    final ScriptEngine engine = new ScriptEngineManager().getEngineByName("graal.js");
    final Invocable invocable = (Invocable) engine;
    
    engine.eval("function process(arg) { return arg.toUpperCase(); };");
    var rr0 = invocable.invokeFunction("process", "abc123xyz"); // rr0 = ABC123XYZ

But when I load/evaluate the next function with the same name, the previous one will be overwritten - logically, since its the same function name.

    engine.eval("function process(arg) { return arg + 'test'; };");
    var rr1 = invocable.invokeFunction("process", "abc123xyz"); // rr1 = abc123xyztest

Solution

  • This is how I would do it.

    The recommended way to use Graal.js is via the polyglot API: https://www.graalvm.org/reference-manual/embed-languages/

    Not the same probably would work with the ScriptEngine API, but here's the example using the polyglot API.

    1. Wrap the function definition in ()
    2. return the functions to Java
    3. Not pictured, but you probably build a map from the column name to a list of functions to invoke on it.
    4. Call the functions on the data.
    import org.graalvm.polyglot.*;
    import org.graalvm.polyglot.proxy.*;
    
    public class HelloPolyglot {
        public static void main(String[] args) {
            System.out.println("Hello Java!");
            try (Context context = Context.create()) {
                Value toUpperCase = context.eval("js", "(function process(arg) { return arg.toUpperCase(); })");
                Value concatTest = context.eval("js", "(function process(arg) { return arg + 'test'; })");
    
                String text = "HelloWorld";
                text = toUpperCase.execute(text).asString();
                text = concatTest.execute(text).asString();
    
                System.out.println(text);
            }
        }
    }
    

    Now, Value.execute() returns a Value, which I for simplicity coerce to a Java String with asString(), but you don't have to do that and you can operate on Value (here's the API for Value: https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Value.html).