Search code examples
javascriptjavajava-8nashorn

Invoking function on Java subclass in Nashorn


I have a JavaScript script that looks something like this:

function run(database) {
    var result = database.query("query", "some resource name");
    //operations on result
    return result;
}

and I have Java code that executes the script that is something like this:

public Object execute(String script, Database database) {
    NashornScriptEngineFactory nsef = new NashornScriptEngineFactory();
    ScriptEngine engine = nsef.getScriptEngine();
    try {
        engine.eval(script);
        Invocable invocable = (Invocable) engine;
        return invocable.invokeFunction("run", database);
    } catch(ScriptException e) {
        throw new RuntimeException(e);
    }
}

Database is an interface which contains several method definitions, but does not contain the query method. I am calling execute with an implementation of Database, call it DatabaseImpl, that does have the query method. This will be polymorphic, and the script is expected to know what methods are available on the Database instance passed to it. I decided against using generics with this since they are erased at runtime and so the JavaScript has no way of using them anyway, so it's up to the script writer to get the types right.

However, when I run this code, I get the following exception:

javax.script.ScriptException: TypeError: database.query is not a function in <eval> at line number 25

Basically, the gist is, I have an object which implements an interface, and call a method that the particular instance implements, but is not part of the interface definition. My impression is that this should still work, but it does not. It doesn't make much sense to me that I would need to subcast within the script to have access to the query method (is that even possible?), so why am I getting this error? Is it because the method isn't available from the interface definition? Is there a workaround?

Thanks.


Solution

  • This is the main class:

    package so;
    import java.io.InputStreamReader;
    import javax.script.Invocable;
    import javax.script.ScriptEngine;
    import javax.script.ScriptEngineManager;
    
    public class Nashorn {
        public static void main(String[] args) {
            try (InputStreamReader in = resource()) {
                ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
                engine.eval(in);
                Invocable invocable = (Invocable) engine;
                Database database = new DatabaseImpl();
                Object x = invocable.invokeFunction("run", database);
                System.out.println(x);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private static InputStreamReader resource() throws Exception {
            return new InputStreamReader(Nashorn.class.getResourceAsStream("db.js"), "utf-8");
        }
    
    }
    

    Interface and implementation

    package so;
    
    public interface Database {
        void connect();
    }
    
    package so;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    public class DatabaseImpl implements Database {
    
        @Override
        public void connect() {
            System.out.println("Connecting");
        }
    
        public List<?> query(String ... stmt){
            List<String> lst = new ArrayList<>(); 
            lst.addAll(Arrays.asList(stmt));
            lst.addAll(Arrays.asList("A","B","C"));
            return lst;
        }
    
    }
    

    The javascript file (so/db.js)

    function run(database) {
        var result = database.query("query", "some resource name");
        //operations on result
        return result;
    }
    

    Running results in:

    [query, some resource name, A, B, C]
    

    It basically works.