NishM
NishM

Reputation: 1726

Nashorn binding issue

I have a java class like below

    public class StaticBean {

        public static String returnString(int num){
            String json = "[{\"name\" : \"John Doe\", \"age\" : 30}]";
            return json;
        }

    }

In the below test, i have two engine instances.

Upon execution, i see that it is not possible to copy the bindings instance onto another and use it for execution in the same engine or different engine. Even if i do copy, the results doesn't match with the ones i get if i use the same engine/binding.

@Test
    public void testParsingStringObjects() {

        Bindings b = engine.createBindings();
        b.put("ndb", getBindingObject(StaticBean.class, engine));

        engine.setBindings(b, ScriptContext.ENGINE_SCOPE);


        String source = "print('Definition in engine instance 1');" 

                + "var SysU = {};\n"

                + "SysU.returnObject = function returnObjectJS(){\n" 

                + "var string = ndb.returnString(1);\n"

                + "return JSON.parse(string);\n" + "}\n"

                + "SysU.returnString = function returnStringJS(){\n" 

                + "var string = ndb.returnString(1);\n"

                + "print('String returned by the java function SysU.returnString() '+string);\n" 

                + "return string;\n" + "};\n"

                + "print('====================Using the same engine instance for execution====================');\n"

                + "(function (){" + "var json = {};\n"

                + "print(\"String Returned in Caller SysU.returnString(): \"+SysU.returnString());\n"

                + "print(\"Object Returned in Caller SysU.returnObject(): \"+SysU.returnObject());\n"

                + "print(\"**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): \"+JSON.stringify(SysU.returnObject()));\n"

                + "print('Adding the object in another ( json.ext = SysU.returnObject();) ...');\n" 

                + "json.ext = SysU.returnObject();\n"

                + "print(\"Added JSON object which is stringified to display JSON.stringify(json): \"+JSON.stringify(json));\n" + "})();";


        try {
            engine.eval(source);

            Bindings oldEngineBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
            Bindings localBindings = engine2.getBindings(ScriptContext.ENGINE_SCOPE);
            Bindings newBindings = engine.createBindings();

            oldEngineBindings.put("fileName","oldEngine");
            localBindings.put("fileName","localEngine");


            newBindings.putAll(oldEngineBindings);
            newBindings.putAll(localBindings);

            oldEngineBindings.putAll(localBindings);

            ScriptContext ctxt = new SimpleScriptContext();
            ctxt.setBindings(oldEngineBindings, ScriptContext.ENGINE_SCOPE);

            engine.setContext(ctxt);

            engine.eval(""
                    + "print('====================Using the same engine with original binding ====================');\n"
                    + "(function (){" + "var json = {};\n"

                    + "print(\"String Returned in Caller SysU.returnString(): \"+SysU.returnString());\n"

                    + "print(\"Object Returned in Caller SysU.returnObject(): \"+SysU.returnObject());\n"

                    + "print(\"**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): \"+JSON.stringify(SysU.returnObject()));\n"

                    + "print('Adding the object in another ( json.ext = SysU.returnObject();) ...');\n" 

                    + "json.ext = SysU.returnObject();\n"

                    + "print(\"Added JSON object which is stringified to display JSON.stringify(json): \"+JSON.stringify(json));\n" + "})();");

            ctxt.setBindings(newBindings, ScriptContext.ENGINE_SCOPE);

            engine.setContext(ctxt);

            engine.eval(""
                    + "print('====================Using the same engine with copied new binding ====================');\n"
                    + "(function (){" + "var json = {};\n"

                    + "print(\"String Returned in Caller SysU.returnString(): \"+SysU.returnString());\n"

                    + "print(\"Object Returned in Caller SysU.returnObject(): \"+SysU.returnObject());\n"

                    + "print(\"**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): \"+JSON.stringify(SysU.returnObject()));\n"

                    + "print('Adding the object in another ( json.ext = SysU.returnObject();) ...');\n" 

                    + "json.ext = SysU.returnObject();\n"

                    + "print(\"Added JSON object which is stringified to display JSON.stringify(json): \"+JSON.stringify(json));\n" + "})();",newBindings);

            ctxt.setBindings(oldEngineBindings, ScriptContext.ENGINE_SCOPE);

            engine2.setContext(ctxt);

            engine2.eval(""
                    + "print('====================Using a different engine instance with original binding ====================');\n"
                    + "(function (){" + "var json = {};\n"

                    + "print(\"String Returned in Caller SysU.returnString(): \"+SysU.returnString());\n"

                    + "print(\"Object Returned in Caller SysU.returnObject(): \"+SysU.returnObject());\n"

                    + "print(\"**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): \"+JSON.stringify(SysU.returnObject()));\n"

                    + "print('Adding the object in another ( json.ext = SysU.returnObject();) ...');\n" 

                    + "json.ext = SysU.returnObject();\n"

                    + "print(\"Added JSON object which is stringified to display JSON.stringify(json): \"+JSON.stringify(json));\n" + "})();");

        } catch (ScriptException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }

Is this an accepted behavior? or a bug?. I should be able to copy the binding and use them in a different scope in the same engine or different engine instance and yield the same results.

I am testing on Java8u101

Results when you run the test. ReturnObject() function seems to fail when the bindings or the engine instance change.

Definition in engine instance 1
====================Using the same engine instance for execution====================
String returned by the java function SysU.returnString() [{"name" : "John Doe", "age" : 30}]
String Returned in Caller SysU.returnString(): [{"name" : "John Doe", "age" : 30}]
Object Returned in Caller SysU.returnObject(): [object Object]
**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): [{"name":"John Doe","age":30}]
Adding the object in another ( json.ext = SysU.returnObject();) ...
Added JSON object which is stringified to display JSON.stringify(json): {"ext":[{"name":"John Doe","age":30}]}
====================Using the same engine with original binding ====================
String returned by the java function SysU.returnString() [{"name" : "John Doe", "age" : 30}]
String Returned in Caller SysU.returnString(): [{"name" : "John Doe", "age" : 30}]
Object Returned in Caller SysU.returnObject(): [object Object]
**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): [{"name":"John Doe","age":30}]
Adding the object in another ( json.ext = SysU.returnObject();) ...
Added JSON object which is stringified to display JSON.stringify(json): {"ext":[{"name":"John Doe","age":30}]}
====================Using the same engine with copied new binding ====================
String returned by the java function SysU.returnString() [{"name" : "John Doe", "age" : 30}]
String Returned in Caller SysU.returnString(): [{"name" : "John Doe", "age" : 30}]
Object Returned in Caller SysU.returnObject(): [object Object]
**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): undefined
Adding the object in another ( json.ext = SysU.returnObject();) ...
Added JSON object which is stringified to display JSON.stringify(json): {}
====================Using a different engine instance with original binding ====================
String returned by the java function SysU.returnString() [{"name" : "John Doe", "age" : 30}]
String Returned in Caller SysU.returnString(): [{"name" : "John Doe", "age" : 30}]
Object Returned in Caller SysU.returnObject(): [object Object]
**Stringified Object Returned in Caller JSON.stringify(SysU.returnObject()): undefined
Adding the object in another ( json.ext = SysU.returnObject();) ...
Added JSON object which is stringified to display JSON.stringify(json): {}

EDIT :-

Found this thread https://bugs.openjdk.java.net/browse/JDK-8067642 . This mentions something about foreign objects being instances of ScriptObjectMirror . I used the typeof operator to display the type of object returned in the cases that failed and succeeded and both times they were ScriptObjectMirror but the stringify works as expected if i use the original bindings object in the context.

EDIT 2:-

Added a very simple test to demonstrate the above. Kinda like a TLDR for the above :) . Executing the below demonstrates that putAll() on a bindings object does not work as we expect it to.

@Test
    public void testParsingObjects() throws ScriptException {

        String source = "var Func = {};\n"
                + "Func.getJavaScriptObject = function(){"
                + "var jsString = '{\"foo\":\"bar\"}';\n"
                + "return JSON.parse(jsString);"
                + "};";


        String executor = "(function(){ "
                + "var obj = Func.getJavaScriptObject();"
                + "print(JSON.stringify(obj));"
                            + " })();";

        System.out.println("Executing source...");
        engine.eval(source);
        System.out.println("\nUsing the same binding instance and engine\n");
        engine.eval(executor);

        Bindings originalBinding = engine.getBindings(ScriptContext.ENGINE_SCOPE);


        Bindings copiedBinding = engine.createBindings();

        copiedBinding.putAll(originalBinding);

        System.out.println("\nUsing the copied binding instance and engine\n");
        engine.eval(executor,copiedBinding);


    }

Result of execution.

Executing source...

Using the same binding instance and engine

{"foo":"bar"}

Using the copied binding instance and engine

undefined

Upvotes: 3

Views: 1886

Answers (2)

user127
user127

Reputation: 111

Here is the code I am using the share compiled JavaScript code between ScriptContext instances used by different threads. The main benefit here is only compiling the code once, although I also benefit from not needing to stream the code from the REST API multiple times. I did not include the REST part for brevity.

import javax.script.CompiledScript; 
import javax.script.ScriptContext; 
import javax.script.ScriptEngine; 
import javax.script.ScriptEngineManager; 
import javax.script.ScriptException; 
import javax.script.SimpleScriptContext; 
import java.util.concurrent.Callable; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Future; 
import java.util.concurrent.ExecutionException; 
import java.util.ArrayList; 
import java.util.List; 

public class ContextDemo {
    static CompiledScript codeLib; 
    static ScriptEngine engine; 
    static ScriptContext context; 
    static List <Future<String>> taskResults; 
    static ExecutorService executor; 
    static List <Callable<String>> tasks = new ArrayList<Callable<String>> (); 

    public static void main(String[] args) {
        try {
            // Initialize workers and execute
            run(4); 
        } catch(InterruptedException | ExecutionException | ScriptException e) {
            System.out.println(e.getMessage()); 
        }
    }

    static void run(int workers) throws InterruptedException, ExecutionException, ScriptException {
        // Initialize engine and initial context
        engine = new ScriptEngineManager().getEngineByName("nashorn"); 
        context = new SimpleScriptContext(); 
        engine.setContext(context); 
        // Compile a JavaScript object with a function
        codeLib = ((javax.script.Compilable)engine).compile("var lib = { func1: function(n, s) { return 'thread number ' + n + ': ' + s; } };"); 
        // Create executor with specified number of workers
        executor = Executors.newFixedThreadPool((int)workers); 
        for (int i = 0; i < workers; i++) {
            tasks.add(workerLambda(i)); 
        }
        // Invoke worker pool
        taskResults = executor.invokeAll(tasks); 
        // Iterate futures list and report results
        for (int i = 0; i < workers; i++) {
            Future < String > f = taskResults.get(i); 
            if (f.isDone()) {
                System.out.println(f.get()); 
            } else {
                System.out.println("Thread " + i + " not done"); 
            }
        }
        // Shutdown the executor
        executor.shutdown(); 
    }

    static Callable <String> workerLambda(int n) {
        int workerNum = n; 
        // Thread-specific script context initialization
        SimpleScriptContext threadContext = new SimpleScriptContext(); 
        threadContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE); 
        try {
            // Inject compiled code library into thread-specific ScriptContext
            codeLib.eval(threadContext); 
        } catch (ScriptException e1) {
            System.out.println(e1.getMessage()); 
        }
        // Return the lambda
        return () ->  {
            // Call the injected object method and return the result
            return (String)engine.eval("lib.func1(" + workerNum + ", 'Hello!');", threadContext); 
        }; 
    }
}

This outputs:

thread number 0: Hello!
thread number 1: Hello!
thread number 2: Hello!
thread number 3: Hello!

Upvotes: 1

user127
user127

Reputation: 111

I wish I could provide a working solution to this, but I am suspecting it may be a Nashorn bug. As evidence, I submit this link to an old JDK bug:

https://bugs.openjdk.java.net/browse/JDK-8023363

Test.java (below) is provided in the link as evidence that both issues (lack of key presence in Map and inability to execute function after .putAll() into new Bindings) are "Not an Issue". Except that I tested the same code and got different results:

  1. The original Bindings only seems to contain the key "nashorn.global", even after the eval
  2. Attempts to execute "func(x)" using the new Bindings (after putAll()) throws a Reference error

Test.java follows:

import javax.script.*;
import java.util.Map;

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

       Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
       scriptEngine.eval("function func(x) { print('I am func ' + x); }");

       // print stuff exposed in engineBindings
       for (Map.Entry<?,?> entry : engineBindings.entrySet()) {
           System.out.println(entry.getKey());
           System.out.println(entry.getValue());
       }

       Bindings localBindings = scriptEngine.createBindings();
       // copy all exposed from other bindings
       localBindings.putAll(engineBindings);
       // put additional variable
       localBindings.put("event", new Object());
       scriptEngine.setBindings(localBindings, ScriptContext.ENGINE_SCOPE);

       scriptEngine.eval("func(event)");
   }
}

Upvotes: 0

Related Questions