I have a server application that loads several script files (for processing specific dataset-fields) at startup. the scripts should be parsed and the "expression"-data of the script should be stored in a map (by column name) so that they can be accessed and executed from there later.
There are two types of scripts. The "simple" ones just contain a process
function, the complex ones currently have a structure similar to the example below (might have more private functions/fields):
// public
function process(input) {
return _doSomething(input);
}
function selfTest() {
if (process("123") !== "123") throw "failed";
// ...
}
// private
var _allowedSymbols = ['H', 'L', 'M'];
function _doSomething(input) {
// _allowedSymbols is used here
}
process
and selfTest
are the "public" functions that will be used by the server-app. selfTest
will be executed once after the file is loaded/evaluated and process
will be executed for incoming data when needed.
I've started with the old JSR 223 way:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("graal.js");
engine.eval("function process(input) { return input.toUpperCase(); }");
// engine.eval("function process(input) { return input + '123'; }");
Invocable inv = (Invocable) engine;
Object result = inv.invokeFunction("process", "peter");
This approach has the problem that the function-data is stored in the javascript engine instance and therefore i can't have multiple "process" methods in my map.
I Could continue that way and dynamically generate name-prefixes for functions and global variables based on the column name... but thats... "ugly"...
I've tried the graalvm-context-way (with help from SO and Oleg, How to store function handles from ScriptManager for later usage?):
var ctx = Context.newBuilder("js").allowAllAccess(false).build();
var src = Source.newBuilder("js", "(function u(input) { return input.toUpperCase(); })", "test.js").build();
var script = ctx.eval(src);
var result = script.execute("peter");
That works for "simple" functions. But for complex scripts the function-expression way from above doesnt work.
EDIT (Solution):
Modified the answer from Oleg slightly and this seems to do the job...
var jsCode = """
(function() {
function process(input) { return input; }
function selfTest() { if (process("123") !== "123") throw "failed"; return true; }
return { process, selfTest };
})();
""";
var ctx2 = Context.newBuilder("js").allowAllAccess(false).build();
Source source = Source.newBuilder("js", jsCode, "test.js").build();
var script = ctx2.eval(source);
var fnProcess = script.getMember("process");
var result = fnProcess.execute("123");
var fnSelfTest = script.getMember("selfTest");
var result2 = fnSelfTest.execute();
It's either functions are declared in the top level namespace, and then name collisions are a problem, or they are in their custom scopes and then you have to have some way to access and call them.
When you're evaluating a source like that:
The result of that evaluation is the last expression in the script. You can think of the line:
Approximately as if it'd be in JS like:
And consequently, you can run it with:
That means you can return multiple functions using helper objects:
Which is exactly what you'd do in JavaScript (I think).
Then you can either get the process function with
And similarly access the
selfTest
function. It might not be ideal that you need to modify the JS sources for that, but I think that's necessary.