Class JavaBox

java.lang.Object
org.dellroad.javabox.JavaBox
All Implemented Interfaces:
Closeable, AutoCloseable

public class JavaBox extends Object implements Closeable
A container for scripts written in the Java language.

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:

Each 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 via Config.Builder.
  • Instances must be initialize()'d before use. This builds the underlying JShell 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 Controls 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.

  • Constructor Details

  • Method Details

    • getConfig

      public Config getConfig()
      Get the Config associated with this instance.
      Returns:
      instance config
    • 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

      public JShell getJShell()
      Get the JShell instanced associated with this container.
      Returns:
      this container's JShell
      Throws:
      IllegalStateException - if this instance is not yet initialized
    • getCurrent

      public static JavaBox getCurrent()
      Get the JavaBox 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 interface AutoCloseable
      Specified by:
      close in interface Closeable
    • getVariable

      public Object getVariable(String varName) throws InterruptedException
      Get the value of a variable in this container.
      Parameters:
      varName - variable name
      Returns:
      variable value
      Throws:
      InterruptedException - if the current thread is interrupted
      IllegalStateException - if this instance is not initialized or closed
      IllegalArgumentException - if varName is not found
      IllegalArgumentException - if varName is not a valid Java identifier
      IllegalArgumentException - if varName is null
      See Also:
    • setVariable

      public void setVariable(String varName, Object varValue) throws InterruptedException
      Declare and assign a variable in this container.

      Equivalent to: setVariable(varName, null, varValue).

      Parameters:
      varName - variable name
      varValue - variable value
      Throws:
      InterruptedException - if the current thread is interrupted
      IllegalStateException - if this instance is not initialized or closed
      IllegalArgumentException - if varName is not a valid Java identifier
      IllegalArgumentException - if varName 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 to varName in subsequent scripts. However, it's possible that varType is not accessible in the script environment, e.g., not on the classpath, or a private class. In that case, this method will throw a JavaBoxException. To avoid that, set varType to any accessible supertype (e.g., "Object"), or use "var" to infer it.

      Parameters:
      varName - variable name
      varType - variable's declared type, or null to infer from actual type; must be accessible in the generated script
      varValue - variable value
      Throws:
      InterruptedException - if the current thread is interrupted
      IllegalStateException - if this instance is not initialized or closed
      IllegalArgumentException - if varName is not a valid Java identifier
      IllegalArgumentException - if varName is null
      JavaBoxException - if variable assignment fails
      See Also:
    • variableValue

      public static Object variableValue()
      Obtain the value of a variable being set by setVariable().

      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 a setVariable() script thread
    • validate

      public ScriptResult validate(String source)
      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 be SnippetOutcome.SuccessfulNoValue.

      Parameters:
      source - the script to execute
      Returns:
      result from script validation
      See Also:
    • compile

      public ScriptResult compile(String source)
      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 be SnippetOutcome.SuccessfulNoValue.

      Parameters:
      source - the script to execute
      Returns:
      result from script compilation
      See Also:
    • execute

      public ScriptResult execute(String source)
      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

      public ScriptResult process(String source, JavaBox.ProcessMode mode)
      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 resulting SnippetOutcomes are returned as a ScriptResult.

      Initial Validation

      In all JavaBox.ProcessModes, an initial basic structural validation of all snippets is first performed, i.e., each snippet is parsed.

      If mode is JavaBox.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 a SnippetOutcome.CompilerError outcome; snippets that successfully parse will have a SnippetOutcome.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, their Snippet.Kind, etc. Note however that in this mode the returned snippets contain only limited information; in particular, they will be unassociated.

      Compilation

      If mode is JavaBox.ProcessMode.COMPILE, then after a successful initial validation each snippet is compiled into bytecode and loaded, which gives any configured Controls 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 a SnippetOutcome.SuccessfulNoValue outcome.

      Execution

      If mode is JavaBox.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 implementing SnippetOutcome.HaltsScript. If this happens, subsequent snippets are not executed and will have outcome SnippetOutcome.Skipped.

      Suspend/Resume

      If an executing script suspends itself by invoking suspend(), this method immediately returns, the corresponding snippet outcome is SnippetOutcome.Suspended, and the script then becomes this instance's suspended script. The script must be resume()ed before a new script can be processed. A suspended script can also be interrupt()ed, in which case it will throw ThreadDeath 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 later resume()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 invoking interrupt().

      Parameters:
      source - the script to process
      mode - processing mode
      Returns:
      result from script processing
      Throws:
      InterruptedException - if the current thread is interrupted or interrupt() is invoked
      IllegalStateException - if this instance has a suspended script
      IllegalStateException - if this instance is not initialized or closed
      IllegalArgumentException - if source or mode is null
    • suspend

      public static Object suspend(Object parameter)
      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() or resume() invocation that (re)started this script's execution will return to the caller, and the corresponding snippet outcome will be a SnippetOutcome.Suspended containing the given parameter.

      This instance will then have a suspended script; it must be resume()ed before a new script can be execute()ed. A suspended script can be interrupt()ed, in which case it will throw ThreadDeath as soon as it resumes.

      Parameters:
      parameter - value to be made available via SnippetOutcome.Suspended.parameter()
      Returns:
      the return value provided to resume()
      Throws:
      ThreadDeath - if interrupt() or close() was invoked while suspended
      IllegalStateException - if the current thread is not a script execution thread
    • resume

      public ScriptResult resume(Object returnValue)
      Resume this instance's suspended script.

      The script's earlier invocation of suspend() will return the given returnValue, or throw ThreadDeath if interrupt() has been invoked since it was suspended. This method will then block just like execute(), 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 invoked suspend(), followed by the updated outcome of the suspended snippet (replacing the previous SnippetOutcome.Suspended outcome), followed by the outcomes of the script's subsequent snippets.

      Parameters:
      returnValue - the value to be returned to the script from suspend()
      Returns:
      the result from the script's execution
      Throws:
      ThreadDeath - if interrupt() or close() was invoked on the associated container
      IllegalStateException - if this instance has no currently suspended script
      IllegalStateException - 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 be SnippetOutcome.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 an SnippetOutcome.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

      public static Control.ExecutionContext executionContextFor(Class<? extends Control> controlType)
      Obtain the execution context associated with the specified Control class and the script execution occurring in the current thread.

      This method can be used by Controls 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 the Control instance itself to allow invoking this method from woven bytecode. The controlType must exactly equal the Control instance class (not just be assignable from it). If multiple instances of the same Control 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 method C.foo() which in turn invokes this method, this method will throw an IllegalStateException 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 type controlType is configured
      IllegalStateException - if the current thread is not a JavaBox 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

      protected void finishingExecution(Object result, Throwable error)
      Subclass hook invoked when finishing script execution from within the snippet thread.

      This method must not lock this instance or deadlock will result.

      The implementation in JavaBox does nothing.