Overview
Jactl.eval()
The simplest way to run a Jactl script is to use the io.jactl.Jactl class and the eval() method:
Object result = Jactl.eval("3 + 4"); // Will return 7
Since this has to compile the script each time, it is not the most efficient way to invoke Jactl scripts and,
due to the way it waits synchronously for a result even if the script does an asynchronous operation, this call
is not generally suitable if running in an event-loop based application.
(The preferred way to run Jactl code is to use Jactl.compile() to compile into a JactlScript object that can
then be invoked multiple times as discussed below).
Sharing Data with Scripts
To share data between the Java application and the script, it is possible to pass in a Map of global variables that the script can then access.
Map<String,Object> globals = new HashMap<>();
globals.put("x", 3);
globals.put("y", 4);
Object result = Jactl.eval("x += y", globals);
int xval = (int)globals.get("x"); // xval will be 7
Jactl supports the following object types as values for the global variables:
- Boolean
- Byte
- Integer
- Long
- Double
- BigDecimal
- String
- List
- Map
Jactl also supports these java.time classes:
- Instant, LocalTime, LocalDate, LocalDateTime, ZonedDateTime, ZoneId, Duration, Period
For List objects, the elements of the List should correspond to one of the listed types. Similarly, Map objects should have keys and values of types that are supported types.
Arrays of these types are also supported.
It is also possible for the value of a variable to be null which can be used as a way to create the variable
when there is no initial value that makes sense for it.
Objects which are instances of a user defined Jactl class are also supported, so, if a previous script invocation has returned such a value, then this same value can be passed to another script invocation. Note that this requires that both invocations use the same JactlContext object (explained later):
JactlContext context = JactlContext.create().build();
Object x = Jactl.eval("class X { int i; def f(n) { i * n } }; new X(2)", Utils.mapOf(), context);
Map<String,Object> globals = new HashMap<>();
globals.put("x", x);
Object result = (int)Jactl.eval("x.f(3)", globals, context);
assertEquals(6, result);
Jactl also supports the ability to configure access to specific "host classes" (application or Java classes) and if so enabled, these types can also be used in the globals bindings (see Allow Host Access).
Setting input/output for scripts
If the script uses the nextLine() function to read lines from some input then you can specify a BufferedReader
for the script's input.
Similarly, you can also supply a PrintStream object that will be where any output from print or println
is sent.
For example:
Map<String,Object> globals = new HashMap<>();
globals.put("prefix", "DEBUG:");
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
PrintStream output = System.out;
Jactl.eval("stream(nextLine).each{ println \"$prefix: $it\\n\" }", globals, input, output);
Jactl.compile()
The preferred way to run Jactl scripts is to compile them using Jactl.compile().
This returns a JactlScript object which can then be run as many times as needed.
JactlScript objects can be run using the eval() or run() methods.
If a scripts perform an asynchronous or blocking operation (for example invoking sleep() or performing a database
operation) then Jactl suspends the script and resumes it once the result is ready (unless async(false) has
been set).
This allows event-loop based applications to run Jactl scrips without worrying about blocking the event-loop thread that
invokes a Jactl script.
The JactlScript.eval() method works like Jactl.eval() in that it waits for the script to complete before returning
the result to the caller.
If you are invoking scripts from an event-loop thread of your application, be aware that this might therefore block
that thread if the script does something asynchronous.
If the threading model of the application requires that the result of an asynchronous operation is processed on the
same event-loop thread that invoked the operation (for example Vert.x based applications), then using eval() with
a script that does something asynchronous will cause the event-loop thread to block forever.
This is because the thread waiting for the script to complete is also the thread that the result needs to be processed
on before the script can return.
Therefore, eval() should only be used if:
- the caller can guarantee that the script does not invoke an asynchronous function, or
- the caller is not running on an event-loop thread, or
- Jactl has been configured with
async(false)to disable asynchronous behaviour.
Map<String,Object> globals = new HashMap<>();
JactlScript script = Jactl.compileScript("3 + 4", globals);
Object result = script.eval(globals); // result will be 7
assertEquals(7, result);
Again, you can use the Map of globals as a way of sharing data between the script and the application.
The globals you pass in at compile time should contain all the global variables that the script will refer to.
The values in the Map can all be null at this point since the compiler just uses the Map to decide if a variable
exists or not.
The globals Map passed into eval() (and run()) will be then be the one that the script
uses at runtime.
This can be a different Map each time but should, obviously, contain an entry for each variable passed in at compile
time, or you will get a runtime error when the script tries to access a global variable not present in the map passed
in.
The run() method should be used in situations where you don't want to block the current thread (for example,
because you are already on an event-loop thread).
It takes two arguments:
- the globals Map for global variables, and
- a completion callback (of type
Consumer<Object>) that will be passed the script result when the script completes.
If the script is entirely synchronous (doesn't use any asynchronous functions like sleep()) then the completion
will be invoked in the current thread before the call to run() returns to the caller.
Here is an example:
Map<String,Object> globals = new HashMap<>();
globals.put("x", null);
globals.put("y", null);
JactlScript script = Jactl.compileScript("x + y", globals);
Map<String,Object> globalValues = new HashMap<>();
globalValues.put("x", 7);
globalValues.put("y", 3);
// Invoke run() with a completion callback
script.run(globalValues, result -> System.out.println("Result is " + result));
// Invoke eval() since we know that this script is synchronous
System.out.println("Result is " + script.eval(globalValues));
Alternatively, you can use the version of run() that returns a Future:
Future<Object> future = script.run(globalValues);
System.out.println("Result is " + future.get());
Using Types in the Globals Map
If you are using the recommended compile/eval process to compile scripts once and then run them multiple times, the globals map you pass to the compile step can be a map of types rather than concrete objects. This allows you to specify the type of the globals in a more flexible way. For example, this allows you to say that a global variable is a type that implements a given interface, which is not possible to do when passing in a concrete object.
There are some static types declared by the Jactl class for the common built-in types:
Jactl.OBJECT_TYPE
Jactl.BOOLEAN_TYPE
Jactl.BYTE_TYPE
Jactl.INT_TYPE
Jactl.LONG_TYPE
Jactl.DOUBLE_TYPE
Jactl.DECIMAL_TYPE
Jactl.STRING_TYPE
Jactl.MAP_TYPE
Jactl.LIST_TYPE
Jactl.INSTANT_TYPE
Jactl.LOCAL_TIME_TYPE
Jactl.LOCAL_DATE_TYPE
Jactl.LOCAL_DATE_TIME_TYPE
Jactl.ZONED_DATE_TIME_TYPE
Jactl.ZONE_ID_TYPE
Jactl.DURATION_TYPE
Jactl.PERIOD_TYPE
Here is an example of how to use these:
Map<String,Object> globals = new HashMap<>();
globals.put("today", Jactl.LOCAL_DATE_TYPE);
globals.put("count", Jactl.INT_TYPE);
JactlScript script = Jactl.compileScript("today.plusDays(count)", globals);
Map<String,Object> values = new HashMap<>();
values.put("today", LocalDate.now());
values.put("count", 1);
LocalDate tomorrow = (LocalDate)script.eval(values);
For other types you want to specify you can use Jactl.type(class) to get a type that
you add to the globals map.
For example:
Map<String,Object> globals = new HashMap<>();
globals.put("arr", Jactl.type(int[].class));
JactlScript script = Jactl.compileScript("arr.filter{ it % 2 }.sum()", globals);
Map<String,Object> values = new HashMap<>();
values.put("arr", new int[]{ 1,2,3,4,5 });
assertEquals(9, script.eval(values));
If you are allowing host class access, then you can also pass in instances of allowed host classes:
JactlContext ctx = JactlContext.create()
.allowHostAccess(true)
.allowHostClassLookup(name -> name.equals("com.acme.Application"))
.build();
var globals = new HashMap<String,Object>();
globals.put("app", Jactl.type(com.acme.Application.class));
JactlScript script = Jactl.compileScript("app.userCount()", globals);
var values = new HashMap<String,Object>();
values.put("app", com.acme.Application.instance());
int count = (int)script.eval(values);
You can pass in objects of classes that have not been enabled with allowHostClassLookup() but
Jactl will generate an error if you try to invoke a method on such an object.
Input/Output
Both run() and eval() have overloaded versions that also accept a Reader and Writer
for the input/output of the script.
For example:
HashMap<String, Object> globals = new HashMap<String,Object>();
globals.put("x", null);
globals.put("y", null);
JactlScript script = Jactl.compileScript("def result = stream(nextLine).map{ it as int }.sum() + x + y\n" +
"println 'Result is ' + result\n" +
"return result",
globals);
HashMap<String, Object> globalValues = new HashMap<String,Object>();
globalValues.put("x", 7);
globalValues.put("y", 3);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Future<Object> future = script.run(globalValues, new BufferedReader(new StringReader("1\n2\n3\n")), new PrintStream(out));
assertEquals(16, future.get());
assertEquals("Result is 16\n", out.toString());
Additional Context for Application Integration
It is possible to configure an application context object on the JactlContext that can be accessed from within custom functions (see Application Context.
In addition, per script invocation context can be passed when invoking JactlScript.eval() or JactlScript.run()
that can then be accessed from within a custom function.
There are multiple eval() and run() variations where this can be passed in.
For example:
Map bindings = new HashMap();
MyInvocationContextClass invocationCtx = new MyInvocationContextClass();
JactlScript script = Jactl.compileScript(...);
Object result = script.eval(bindings, invocationCtx);
To access this context within a custom function:
public static Object myFunction() {
RuntimeState state = RuntimeState.getState();
MyInvocationContextClass invocationCtx = (MyInvocationContextClass) state.getInvocationContext();
...
}
Errors
If an error is detected at compile time then an exception of type io.jactl.CompileError will be thrown.
If a runtime error occurs then an exception of type io.jactl.runtime.RuntimeError will be thrown.
A special subclass of RuntimeError called io.jactl.runtime.DieError will be thrown if the script invokes the die statement.
All these exception classes are subclasses of the top level io.jactl.JactlError.
All of these classes are unchecked exceptions so be sure to catch them at the appropriate place in your code.
Default Execution Environment and Shutdown
By default, if you have not provided an implementation of the JactlEnv interface
(see Jactl Execution Environment below) you will be using the
built-in io.jactl.DefaultEnv.
This class creates static thread pools for the non-blocking event loop threads, the blocking threads, and a thread for timers.
Since these thread-pools are daemon threads, if you want to cleanly exit without invoking
System.exit(), you will need stop these thread-pools using the static io.jactl.DefaultEnv.shutdown()
method:
class MyTest {
public static void main(String[] args) {
Object result = Jactl.eval("3 + 5");
System.out.println("Result is " + result);
io.jactl.DefaultEnv.shutdown();
}
}
ScriptEngine API
In addition to the integration mechanism described above, Jactl also supports the standard API for scripting: JSR 223 Scripting for the Java Platform. This API provides a standardised way to invoke scripting languages from a Java application.
For example:
import javax.script.*;
class JSR223Test {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager engineMgr = new ScriptEngineManager();
ScriptEngine engine = engineMgr.getEngineByName("jactl");
engine.put("x", 3); // engine binding scope
engineMgr.put("z", 5); // global binding scope
Object result = engine.eval("x + z");
System.out.println("Result is " + result);
}
}
The JactlScriptEngine implements the optional Invocable and Compilable interfaces.
See Java Scripting API for more detail about invoking Jactl scripts using this API.