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 configured for 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.

Bevcause JavaBox instances always run in local execution mode, they support the direct transfer of Java objects between the container and the outside world:

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".

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.

If there is a suspended script associated with an instance, any new invocation of execute() will fail. Instead, suspeneded scripts must be resumed via resume() and allowed to terminate.

Interruption

Both 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 equivalenntly by invoking interrupt(). If a snippet's execution is interrrupted, its outcome is SnippetOutcome.Interrupted.

A suspended script can also be interrupted, but the script does not awaken immediately. Instead, upon the next call to resume(), it will terminate immediately and 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
  • 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.

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. The one-at-a-time operations are:

  • 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 JShell initialization and script 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, or was never initialized, 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
    • execute

      public ScriptResult execute(String source)
      Execute the given script in this container.

      The script is broken into individual snippets, which are executed one at a time. Processing stops after the last snippet, or when any snippet has an outcome implementing SnippetOutcome.HaltsScript. The results from the execution of all snippets attempted up to that point are then returned as a ScriptResult.

      If the current thread is interrupted, then interrupt() is implicitly invoked.

      If the script suspend()s itself, this method returns as described above, the last outcome will be a SnippetOutcome.Suspended, and the script becomes this instance's suspended script. The script must be resume()ed before a new script can be executed. A suspended script can be interrupt()ed, in which case it will throw ThreadDeath as soon as it is resumed.

      If this method is invoked when this instance has a suspended script, an IllegalStateException is thrown.

      Parameters:
      source - the script to execute
      Returns:
      result from successful script execution
      Throws:
      InterruptedException - if the current thread is interrupted
      IllegalStateException - if this instance has a suspended script
      IllegalStateException - if this instance is not initialized or closed
      IllegalArgumentException - if source is null
    • suspend

      public static Object suspend(Object parameter)
      Suspend the script 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, with the corresponding snippet outcome being a SnippetOutcome.Suspended containing parameter.

      This instance will then have a suspended script. The suspended script 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 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, followed by any additional outcomes from the script.

      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 suspeneded script, that script will awaken and throw an immediate ThreadDeath exception. Note that SnippetOutcome.Interrupted is the outcome assigned to any snippet that terminates by throwing ThreadDeath.

      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.

      If a return value from a script's execution is an invokable object, then any subsequent invocations into that object's methods will not be able to obtain any per-execution context using this method, because they will executing outside of the container. Instead, an IllegalStateException is thrown.

      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.

      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.

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

      The implementation in JavaBox does nothing.