Class JavaBox
- All Implemented Interfaces:
Closeable
,AutoCloseable
Overview
Each JavaBox
instance relies on an underlying JShell
instance to parse and execute
scripts and to hold its state, i.e., the variables, methods, and classes declared by those scripts.
The JavaBox
instance is configured for local execution, so scripts
run within the same virtual machine.
Because JavaBox
instances always run in local execution mode, they support the direct transfer of Java objects
between the container and the outside world:
- Return values from script execution are returned to the caller.
JShell
variables can be read and written directly (seegetVariable()
andsetVariable()
).
JavaBox
instance uses a separate ClassLoader
, so generated classes can be garbage
collected when the container is closed.
Lifecycle
Usage follows this pattern:
- Instances are created by providing a
Config
, an immutable configuration object created viaConfig.Builder
. - Instances must be
initialize()
'd before use. This builds the underlyingJShell
instance and creates an internal service thread. - Instances must be
close()
'd to release resources when no longer needed.
Script Execution
Scripts are executed via execute()
. A single script may contain multiple individual expressions,
statements, or declarations; these are called "snippets". The snippets are analyzed and executed one at a time.
The return value from execute()
contains a distinct SnippetOutcome
for each of the snippets.
Often the last snippet's return value (if any) is considered to be the overall script's "return value".
Script Validation
It is also possible to do basic validationt and/or fully compile a script without executing it. This allows the caller to
gather some basic information about the script and the snippets it contains, and verify correctness.
See process()
for details.
Suspend and Resume
If a script invokes suspend()
, then execute()
returns to the caller with the
last snippet outcome being an instance of SnippetOutcome.Suspended
. The script can be restarted later by invoking
resume()
, which behaves just like execute()
, except that it continues the previous
script instead of starting a new one. On the next return, the previously suspended snippet's earlier SnippetOutcome.Suspended
outcome will be overwritten with its new, updated outcome.
As long as there is a suspended script associated with an instance, additional invocations of execute()
will fail. Instead, suspended scripts must first be resumed via resume()
and allowed to terminate
(even if they are interrupted; see below).
Interruption
Both methods execute()
and resume()
block the calling thread until the script terminates
or suspends itself. Another thread can interrupt that execution by interrupting the calling thread, or equivalently
by invoking interrupt()
. If a snippet's execution is interrrupted, the original thread will return and the snippet's
outcome will be SnippetOutcome.Interrupted
.
A suspended script can also be interrupted, but the script does not awaken immediately. Instead, it must be resumed first
before the interrupt can take effect. Upon the next call to resume()
, the script will terminate immediately
and the interrupted snippet will have outcome SnippetOutcome.Interrupted
.
Controls
Scripts may be restricted or otherwise transformed using Control
s which are specified as part of the initial
Config
. Controls can do the following things:
- Inspect and modify all of the bytecode generated from scripts
- Veto a snippet before or during execution by throwing a
ControlViolationException
- Keep state associated with each
JavaBox
instance - Keep state associated with each
JavaBox
snippet execution
Every control is given a per-container Control.ContainerContext
and a per-execution Control.ExecutionContext
.
Controls can modify script bytecode to insert method calls into the control itself, where it can then utilize its
state to decide what to do, etc. To access its state from within an executing snippet, a control invokes
executionContextFor()
.
Examples
Here is a simple "Hello, World" example:
Config config = Config.builder().build();
try (JavaBox box = new JavaBox(config)) {
box.initialize();
box.setVariable("target", "World");
ScriptResult result = box.execute("""
String.format("Hello, %s!", target);
""");
System.out.println(result.returnValue()); // prints "Hello, World!"
}
Here is an example that shows how to avoid infinite loops:
// Set up control
Config config = Config.builder()
.withControl(new TimeLimitControl(Duration.ofSeconds(5)))
.build();
// Execute script
ScriptResult result;
try (JavaBox box = new JavaBox(config)) {
box.initialize();
result = box.execute("""
while (true) {
Thread.yield();
}
""");
}
// Check result
switch (result.snippetOutcomes().get(0)) {
case SnippetOutcome.ExceptionThrown e when e.exception() instanceof TimeLimitExceededException
-> System.out.println("infinite loop detected");
}
Thread Safety
Instances are thread safe but single threaded: when simultaneous operations are attempted from multiple threads, only one operation executes at a time.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enum
Specifies the snippet processing mode to use when invokingprocess()
. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoid
close()
Close this instance.Compile the given script but do not execute it.Execute the given script in this container.static Control.ExecutionContext
executionContextFor
(Class<? extends Control> controlType) Obtain the execution context associated with the specifiedControl
class and the script execution occurring in the current thread.protected void
finishingExecution
(Object result, Throwable error) Subclass hook invoked when finishing script execution from within the snippet thread.Get theConfig
associated with this instance.static JavaBox
Get theJavaBox
instance associated with the current thread.Get theJShell
instanced associated with this container.getVariable
(String varName) Get the value of a variable in this container.void
Initialize this instance.boolean
Interrupt the current script execution, if any.boolean
isClosed()
Determine if this instance is closed.boolean
Determine if this instance is currently executing a script.boolean
Determine if this instance has been initialized.boolean
Determine if this instance has a suspended script.process
(String source, JavaBox.ProcessMode mode) Process the given script in this container.Resume this instance's suspended script.void
setVariable
(String varName, Object varValue) Declare and assign a variable in this container.void
setVariable
(String varName, String varType, Object varValue) Declare and assign a variable in this container.protected void
Subclass hook invoked when starting script execution from within the snippet thread.static Object
Suspend the script that is currently executing in the current thread.Validate the given script is parseable.static Object
Obtain the value of a variable being set bysetVariable()
.
-
Constructor Details
-
JavaBox
Constructor.Instances must be
initialize()
d before use.- Parameters:
config
- configuration- Throws:
IllegalArgumentException
- ifconfig
is null
-
-
Method Details
-
getConfig
-
isInitialized
public boolean isInitialized()Determine if this instance has been initialized.- Returns:
- true if this instance is initialized, otherwise false
-
isClosed
public boolean isClosed()Determine if this instance is closed.- Returns:
- true if this instance is closed, otherwise false
-
isExecuting
public boolean isExecuting()Determine if this instance is currently executing a script.A suspended script counts as "currently executing"; use
isSuspended()
to detect that situation.- Returns:
- true if this instance has a currently executing script, otherwise false
-
isSuspended
public boolean isSuspended()Determine if this instance has a suspended script.- Returns:
- true if this instance has a currently suspended script, otherwise false
-
getJShell
Get theJShell
instanced associated with this container.- Returns:
- this container's
JShell
- Throws:
IllegalStateException
- if this instance is not yet initialized
-
getCurrent
Get theJavaBox
instance associated with the current thread.This method works during (a)
JShell
initialization and (b) snippet execution.- Throws:
IllegalStateException
- if there is no such instance
-
initialize
public void initialize()Initialize this instance.- Throws:
IllegalStateException
- if this instance is already initialized or closed
-
close
public void close()Close this instance.If this instance is already closed this method does nothing.
If there is a currently executing or suspended script, it will be interrupted and allowed to terminate.
- Specified by:
close
in interfaceAutoCloseable
- Specified by:
close
in interfaceCloseable
-
getVariable
Get the value of a variable in this container.- Parameters:
varName
- variable name- Returns:
- variable value
- Throws:
InterruptedException
- if the current thread is interruptedIllegalStateException
- if this instance is not initialized or closedIllegalArgumentException
- ifvarName
is not foundIllegalArgumentException
- ifvarName
is not a valid Java identifierIllegalArgumentException
- ifvarName
is null- See Also:
-
setVariable
Declare and assign a variable in this container.Equivalent to:
setVariable
(varName, null, varValue)
.- Parameters:
varName
- variable namevarValue
- variable value- Throws:
InterruptedException
- if the current thread is interruptedIllegalStateException
- if this instance is not initialized or closedIllegalArgumentException
- ifvarName
is not a valid Java identifierIllegalArgumentException
- ifvarName
is null- See Also:
-
setVariable
public void setVariable(String varName, String varType, Object varValue) throws InterruptedException Declare and assign a variable in this container.This is basically equivalent to executing the script
"<varType> <varName> = <varValue>;"
.If
vartype
is null:- The actual type of
varValue
(expressed as a string) will be used; this type name must be accessible in the generated script - If
varValue
is a non-null primitive wrapper type, the corresponding primitive type is used - If
varValue
is null,var
will be used
Using the narrowest possible type for
varType
is advantageous because it eliminates the need for casting when referring tovarName
in subsequent scripts. However, it's possible thatvarType
is not accessible in the script environment, e.g., not on the classpath, or a private class. In that case, this method will throw aJavaBoxException
. To avoid that, setvarType
to any accessible supertype (e.g.,"Object"
), or use"var"
to infer it.- Parameters:
varName
- variable namevarType
- variable's declared type, or null to infer from actual type; must be accessible in the generated scriptvarValue
- variable value- Throws:
InterruptedException
- if the current thread is interruptedIllegalStateException
- if this instance is not initialized or closedIllegalArgumentException
- ifvarName
is not a valid Java identifierIllegalArgumentException
- ifvarName
is nullJavaBoxException
- if variable assignment fails- See Also:
- The actual type of
-
variableValue
Obtain the value of a variable being set bysetVariable()
.This method is only used internally; it's
public
so that it can be accessed from JShell scripts.- Returns:
- value of variable being set if any, otherwise null
- Throws:
IllegalStateException
- if the current thread is not asetVariable()
script thread
-
validate
Validate the given script is parseable.This is a convenience method, equivalent to:
process
(source,
JavaBox.ProcessMode.COMPILE
)
. If compilation is successful, all outcomes will beSnippetOutcome.SuccessfulNoValue
.- Parameters:
source
- the script to execute- Returns:
- result from script validation
- See Also:
-
compile
Compile the given script but do not execute it.This is a convenience method, equivalent to:
process
(source,
JavaBox.ProcessMode.COMPILE
)
. If compilation is successful, all outcomes will beSnippetOutcome.SuccessfulNoValue
.- Parameters:
source
- the script to execute- Returns:
- result from script compilation
- See Also:
-
execute
Execute the given script in this container.This is a convenience method, equivalent to:
process
(source,
JavaBox.ProcessMode.EXECUTE
)
.- Parameters:
source
- the script to execute- Returns:
- result from script execution
- See Also:
-
process
Process the given script in this container.The script is broken into individual snippets, each of which is validated, compiled, and/or executed according to the specified
JavaBox.ProcessMode
, and the resultingSnippetOutcome
s are returned as aScriptResult
.Initial Validation
In all
JavaBox.ProcessMode
s, an initial basic structural validation of all snippets is first performed, i.e., each snippet is parsed.If
mode
isJavaBox.ProcessMode.VALIDATE
, or any snippet fails to parse, then processing stops after this step and all snippet outcomes are returned. Snippets that fail to parse will have aSnippetOutcome.CompilerError
outcome; snippets that successfully parse will have aSnippetOutcome.SuccessfulNoValue
outcome.JavaBox.ProcessMode.VALIDATE
can be used to quickly gather basic information about a script, including how many snippets it contains, their source code offsets, theirSnippet.Kind
, etc. Note however that in this mode the returned snippets contain only limited information; in particular, they will be unassociated.Compilation
If
mode
isJavaBox.ProcessMode.COMPILE
, then after a successful initial validation each snippet is compiled into bytecode and loaded, which gives any configuredControl
s a chance to veto it, but no execution of any snippet's generated bytecode occurs. All snippet outcomes are then returned; snippets that compile successfully will have aSnippetOutcome.SuccessfulNoValue
outcome.Execution
If
mode
isJavaBox.ProcessMode.EXECUTE
, then after a successful initial validation each snippet is compiled, loaded, and executed, one at a time. Processing stops if compilation or execution of any snippet results in an error, i.e., it has an outcome implementingSnippetOutcome.HaltsScript
. If this happens, subsequent snippets are not executed and will have outcomeSnippetOutcome.Skipped
.Suspend/Resume
If an executing script suspends itself by invoking
suspend()
, this method immediately returns, the corresponding snippet outcome isSnippetOutcome.Suspended
, and the script then becomes this instance's suspended script. The script must beresume()
ed before a new script can be processed. A suspended script can also beinterrupt()
ed, in which case it will throwThreadDeath
as soon as it is resumed.When this method returns early due to suspension, subsequent snippets will have outcome
SnippetOutcome.Skipped
. If the script is laterresume()
ed, these outcomes are updated accordingly in the return value from that method.If this method is invoked when this instance already has a suspended script, an
IllegalStateException
is thrown.Interruption
This method blocks until the script completes, suspends itself, or
interrupt()
is invoked (typically from another thraed). Interrupting the current thread has the same effect as invokinginterrupt()
.- Parameters:
source
- the script to processmode
- processing mode- Returns:
- result from script processing
- Throws:
InterruptedException
- if the current thread is interrupted orinterrupt()
is invokedIllegalStateException
- if this instance has a suspended scriptIllegalStateException
- if this instance is not initialized or closedIllegalArgumentException
- ifsource
ormode
is null
-
suspend
Suspend the script that is currently executing in the current thread.If a script invokes this method, the script will pause and the associated
execute()
orresume()
invocation that (re)started this script's execution will return to the caller, and the corresponding snippet outcome will be aSnippetOutcome.Suspended
containing the givenparameter
.This instance will then have a suspended script; it must be
resume()
ed before a new script can beexecute()
ed. A suspended script can beinterrupt()
ed, in which case it will throwThreadDeath
as soon as it resumes.- Parameters:
parameter
- value to be made available viaSnippetOutcome.Suspended.parameter()
- Returns:
- the return value provided to
resume()
- Throws:
ThreadDeath
- ifinterrupt()
orclose()
was invoked while suspendedIllegalStateException
- if the current thread is not a script execution thread
-
resume
Resume this instance's suspended script.The script's earlier invocation of
suspend()
will return the givenreturnValue
, or throwThreadDeath
ifinterrupt()
has been invoked since it was suspended. This method will then block just likeexecute()
, i.e., until the script terminates or suspends itself again.The returned
ScriptResult
will include the outcomes from all snippets in the original script, including those that executed before the snippet that invokedsuspend()
, followed by the updated outcome of the suspended snippet (replacing the previousSnippetOutcome.Suspended
outcome), followed by the outcomes of the script's subsequent snippets.- Parameters:
returnValue
- the value to be returned to the script fromsuspend()
- Returns:
- the result from the script's execution
- Throws:
ThreadDeath
- ifinterrupt()
orclose()
was invoked on the associated containerIllegalStateException
- if this instance has no currently suspended scriptIllegalStateException
- if this instance is not initialized or closed
-
interrupt
public boolean interrupt()Interrupt the current script execution, if any.If there is no current execution, then nothing happens and false is returned. Otherwise, an attempt is made to stop the execution via
JShell.stop()
. If successful, the final snippet outcome will beSnippetOutcome.Interrupted
. Even in this case, because this operation is asynchronous, the snippet may have actually never started, it may have only partially completed, or it may have fully completed.If this instance has a currently suspended script, that script will awaken, throw an immediate
ThreadDeath
exception, and return anSnippetOutcome.Interrupted
outcome.If this instance is closed or not initialized, false is returned.
- Returns:
- true if execution was interrupted, false if no execution was occurring
-
executionContextFor
Obtain the execution context associated with the specifiedControl
class and the script execution occurring in the current thread.This method can be used by
Control
s that need access to the per-execution or per-container context from within the execution thread, for example, from bytecode woven into script classes.The
Control
class is used instead of theControl
instance itself to allow invoking this method from woven bytecode. ThecontrolType
must exactly equal theControl
instance class (not just be assignable from it). If multiple instances of the sameControl
class are configured on a container, then the context associated with the first instance is returned.This method only works while executing within the container. For example, if a scripts defines and returns an instance of some class
C
, and then the caller invokes a methodC.foo()
which in turn invokes this method, this method will throw anIllegalStateException
because it will be executing outside of the container. In other words, controls that use this method must make other arrangements if they want to obtain context when executing script code outside of the container.- Parameters:
controlType
- the control's Java class- Returns:
- the execution context for the control of type
controlType
- Throws:
JavaBoxException
- if no control having typecontrolType
is configuredIllegalStateException
- if the current thread is not aJavaBox
script execution thread
-
startingExecution
protected void startingExecution()Subclass hook invoked when starting script execution from within the snippet thread.This method must not lock this instance or deadlock will result.
The implementation in
JavaBox
does nothing. -
finishingExecution
-