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
For Maps, the keys must be Strings and for both Lists and Maps, the values in the List or Map should be one of the types listed above.
It is also allowed that the value of a variable is 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);
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 runSync()
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.
This allows event-loop based applications to run Jactl scrips without worrying about blocking the event-loop thread that
invokes a Jactl script.
The runSync()
method works like eval()
in that it waits for the script to complete before returning the result to
the caller.
If 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 runSync()
will
cause the event-loop thread to block forever if the script does something asynchronous since 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.
This is why runSync()
should only be used if the caller can guarantee that the script does not invoke an asynchronous
function or if the caller is not running on an event-loop thread.
Map<String,Object> globals = new HashMap<>();
JactlScript script = Jactl.compileScript("3 + 4", globals);
Object result = script.runSync(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 runSync()
(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));
Input/Output
Both run()
and runSync()
have overloaded verisons that also accept a BufferedReader
and PrintStream
for the input/output of the script.
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();
}
}