Java Target Guide

Java Target Guide

Complete guide for generating and running SCXML state machines on the JVM.

Table of Contents

  1. Overview
  2. Quick Start
  3. Code Generation
  4. Dependencies
  5. Datamodel Support
  6. Executor Patterns
  7. Event Processing
  8. Invoke Children
  9. Configuration Reference
  10. Native-Image Support
  11. API Reference
  12. Troubleshooting

Overview

The Java target generates high-performance, type-safe Java classes from SCXML state machines.

Key Features

Feature Description
Type Safe Generated classes with compile-time event constants
O(1) Dispatch Integer-based event IDs for fast dispatch
Thread Safe Built-in executor for multi-threaded environments
W3C Compliant 100% ECMAScript datamodel (183/183 tests)
Multiple Engines Rhino (default) or GraalJS for ECMAScript

Supported Datamodels

Datamodel Engine Use Case
ecmascript Rhino (default) Full JavaScript scripting
graaljs GraalJS Native-image compatible
xpath Saxon-HE XML-centric applications
null Built-in Minimal overhead, In() only
native-java Compile-time Type-safe Java fields

Quick Start

1. Generate Code

bash
scxml-gen traffic.scxml -o TrafficLight.java --package com.example

2. Add Dependencies

gradle
dependencies {
    implementation 'eu.mihosoft.scxml:scxml-core:0.1.0-SNAPSHOT'
    implementation 'org.mozilla:rhino:1.7.15'  // ECMAScript support
}

3. Use in Application

java
import com.scxmlgen.runtime.executor.ContinuousStateMachineExecutor;
import static com.example.TrafficLight.*;

public class Main {
    public static void main(String[] args) {
        // 1. Create state machine
        TrafficLight machine = new TrafficLight();

        // 2. Create and start executor (handles delayed events)
        ContinuousStateMachineExecutor executor =
            new ContinuousStateMachineExecutor(machine);
        executor.start();

        // 3. Send events using O(1) integer constants
        executor.send(EVT_TIMER);

        // 4. Check state
        if (machine.isInState("green")) {
            System.out.println("Go!");
        }

        // 5. Cleanup
        executor.close();
    }
}

Code Generation

Basic Generation

bash
# Generate Java class with package
scxml-gen traffic.scxml -o TrafficLight.java --package com.example

# Override class name
scxml-gen traffic.scxml -o MyTrafficLight.java --class MyTrafficLight --package com.app

# Generate without package (default package)
scxml-gen traffic.scxml -o TrafficLight.java

Generated Files

For traffic.scxml with --package com.example:

TrafficLight.java
├── package com.example;
├── class TrafficLight extends TranspiledStateMachine
│   ├── // Event constants
│   ├── public static final int EVT_TIMER = 1;
│   ├── public static final int EVT_EMERGENCY = 2;
│   │
│   ├── // State constants
│   ├── private static final int S_RED = 0;
│   ├── private static final int S_GREEN = 1;
│   │
│   ├── // Lifecycle
│   ├── public void start()
│   ├── public void send(int eventId)
│   └── public boolean isInState(String stateId)

Invoke Children Bundling

When using <invoke> with external sources:

bash
# Auto-discover and bundle all children
scxml-gen parent.scxml -o Parent.java --package com.app --bundle-auto

# Manual bundling
scxml-gen parent.scxml -o Parent.java --package com.app \
    --bundle child1.scxml --bundle child2.scxml

Dependencies

Gradle

gradle
dependencies {
    // Core runtime (required)
    implementation 'eu.mihosoft.scxml:scxml-core:0.1.0-SNAPSHOT'

    // ECMAScript support - choose ONE:
    implementation 'org.mozilla:rhino:1.7.15'           // Rhino (default)
    // OR
    implementation 'org.graalvm.polyglot:polyglot:24.1.1'     // GraalJS
    implementation 'org.graalvm.polyglot:js-community:24.1.1'

    // XPath support (optional)
    implementation 'net.sf.saxon:Saxon-HE:12.4'
}

Maven

xml
<dependencies>
    <dependency>
        <groupId>eu.mihosoft.scxml</groupId>
        <artifactId>scxml-core</artifactId>
        <version>0.1.0-SNAPSHOT</version>
    </dependency>

    <!-- ECMAScript: Rhino (default) -->
    <dependency>
        <groupId>org.mozilla</groupId>
        <artifactId>rhino</artifactId>
        <version>1.7.15</version>
    </dependency>
</dependencies>

Dependency Matrix

Feature Required Dependencies
Basic (null datamodel) scxml-core only
ECMAScript (Rhino) scxml-core + rhino
ECMAScript (GraalJS) scxml-core + polyglot + js-community
XPath scxml-core + Saxon-HE
Native-Java scxml-core only

Datamodel Support

ECMAScript (Default)

Full JavaScript scripting via Rhino or GraalJS.

xml
<scxml datamodel="ecmascript" initial="start">
    <datamodel>
        <data id="count" expr="0"/>
        <data id="items" expr="[]"/>
    </datamodel>
    <state id="start">
        <onentry>
            <script>count++; items.push('item1');</script>
        </onentry>
        <transition cond="count > 5" target="done"/>
    </state>
</scxml>

Switching ECMAScript Engines

java
import com.scxmlgen.runtime.DataModelFactory;
import com.scxmlgen.runtime.DataModelType;

// At startup, BEFORE creating state machines:

// Use GraalJS instead of Rhino
DataModelFactory.setOverride(DataModelType.ECMASCRIPT, DataModelType.GRAALJS);

// Custom implementation
DataModelFactory.setSupplier(DataModelType.ECMASCRIPT, () -> new MyJsEngine());

// Reset to defaults
DataModelFactory.resetConfiguration();

Native-Java Datamodel

Type-safe Java fields with compile-time checking.

xml
<scxml datamodel="native-java" initial="idle">
    <datamodel>
        <data id="counter" type="int" expr="0"/>
        <data id="name" type="String" expr="'Default'"/>
        <data id="active" type="boolean" expr="false"/>
    </datamodel>
    <state id="idle">
        <transition cond="counter > 10" target="done"/>
    </state>
</scxml>

Supported types: int, long, double, boolean, String

Null Datamodel

Minimal overhead - only In() predicate supported.

xml
<scxml datamodel="null" initial="off">
    <state id="off">
        <transition event="toggle" target="on"/>
    </state>
    <state id="on">
        <transition event="toggle" target="off"/>
        <transition cond="In('on')" event="check" target="on"/>
    </state>
</scxml>

XPath Datamodel

For XML-centric applications (87.8% W3C compliance).

xml
<scxml datamodel="xpath" initial="start">
    <datamodel>
        <data id="doc"><root><item>value</item></root></data>
    </datamodel>
    <state id="start">
        <transition cond="$doc/root/item = 'value'" target="found"/>
    </state>
</scxml>

Executor Patterns

Why Executors Are Needed

Unlike JavaScript with a native event loop, Java requires explicit executor management for:

  • Delayed events (<send delay="1s">)
  • Invoke child machine communication
  • Background event processing
  • Thread-safe event queuing

Executor Comparison

Feature ContinuousStateMachineExecutor RunToCompletionStateMachineExecutor
Threading Background daemon thread Caller's thread only
send() Queues, processes async Synchronous on caller thread
Delayed events Automatic Manual processDelayedEvents()
Thread safety Built-in Single-threaded
Use case Production, real-time Unit tests, deterministic

ContinuousStateMachineExecutor (Production)

java
// Create executor with background thread
ContinuousStateMachineExecutor executor =
    new ContinuousStateMachineExecutor(machine);
executor.start();  // Starts daemon thread

// Events are queued and processed asynchronously
executor.send(EVT_START);

// Async with CompletableFuture
CompletableFuture<Void> future = executor.sendAsync(event);
future.thenRun(() -> System.out.println("Event processed"));

// Graceful shutdown
executor.close();

RunToCompletionStateMachineExecutor (Testing)

java
// Synchronous execution for deterministic testing
RunToCompletionStateMachineExecutor executor =
    new RunToCompletionStateMachineExecutor(machine);
executor.start();

// Events processed immediately, synchronously
machine.send(EVT_STEP1);  // Completes before returning
machine.send(EVT_STEP2);  // Deterministic order

// Manual delayed event processing
while (!machine.isFinished()) {
    machine.processDelayedEvents();
    Thread.sleep(10);
}

executor.close();

Tracing

Full tracing support for debugging, analysis, and state machine monitoring.

JsonlTraceWriter (JSONL Output)

java
import com.scxmlgen.runtime.trace.JsonlTraceWriter;

// Create trace writer
try (JsonlTraceWriter writer = new JsonlTraceWriter("trace.jsonl")) {
    TrafficLight machine = new TrafficLight();
    machine.setTraceListener(writer);

    ContinuousStateMachineExecutor executor =
        new ContinuousStateMachineExecutor(machine);
    executor.start();

    executor.send(EVT_TIMER);
    // Trace records all state changes to JSONL file

    executor.close();
}

Custom TraceListener

java
import com.scxmlgen.runtime.trace.ITraceListener;
import com.scxmlgen.runtime.trace.TraceAdapter;

// Extend TraceAdapter to implement only needed methods
public class MyTraceListener extends TraceAdapter {
    @Override
    public void onStateEnter(String stateId, long timestampMicros) {
        System.out.println("Entering: " + stateId);
    }

    @Override
    public void onStateExit(String stateId, long timestampMicros) {
        System.out.println("Exiting: " + stateId);
    }

    @Override
    public void onTransitionFired(String from, String to, String event, long timestampMicros) {
        System.out.println("Transition: " + from + " -> " + to + " on " + event);
    }
}

// Usage
TrafficLight machine = new TrafficLight();
machine.setTraceListener(new MyTraceListener());

Trace API

Class Description
ITraceListener Interface for all trace listeners
IInvokeAwareTraceListener Extended interface with invoke context
TraceAdapter Base class with empty implementations
JsonlTraceWriter JSONL file output (cross-platform)
ConsoleTraceListener Console debugging output

Event Processing

Generated constants provide O(1) event dispatch:

java
import static com.example.TrafficLight.*;

// Fast - integer comparison, no HashMap lookup
executor.send(EVT_TIMER);
executor.send(EVT_EMERGENCY, emergencyData);

// Slower - requires string lookup
executor.send("timer", null);

Generated Constants

java
// Each state machine generates:
public static final int EVT_TIMER = 1;
public static final int EVT_EMERGENCY = 2;
public static final int EVT_BUTTON_CLICK = 3;  // Dots become underscores
public static final int EVT_UNKNOWN = 99999;   // For dynamic events

Dot Notation Events

SCXML hierarchical events map to underscored constants:

SCXML Event Generated Constant
button.click EVT_BUTTON_CLICK
error.io.read EVT_ERROR_IO_READ
done.invoke.child EVT_DONE_INVOKE_CHILD

Prefix Matching: A transition for event="button" matches button, button.click, button.hover, etc.

Event Data Payloads

java
// Send event with data
Map<String, Object> data = Map.of("x", 10, "y", 20);
executor.send(EVT_CLICK, data);

// Access in SCXML
// <transition event="click">
//   <script>var x = _event.data.x;</script>
// </transition>

Invoke Children

Static Invoke Factory

For native-image and controlled environments:

java
import com.scxmlgen.runtime.StaticInvokeFactory;

// Register child machines at startup
StaticInvokeFactory factory = new StaticInvokeFactory();
factory.register("file:child1.scxml", Child1Machine::new);
factory.register("file:child2.scxml", Child2Machine::new);

// Set as default
InvokeFactory.setDefault(factory);

// Now parent machine can invoke children
ParentMachine parent = new ParentMachine();
parent.start();  // Invokes use registered factories

Runtime Invoke Factory

For dynamic SCXML loading (not native-image compatible):

java
// Default behavior - compiles SCXML at runtime
InvokeFactory.setDefault(new RuntimeInvokeFactory());

Configuration Reference

Java Object Binding

Register Java objects for use in ECMAScript:

java
// Initialize datamodel first
machine.initialize();

// Register objects before start
machine.getDataModel().set("logger", new MyLogger());
machine.getDataModel().set("database", databaseService);

// Then start
executor.start();
xml
<!-- Access in SCXML -->
<onentry>
    <script>
        logger.info("State entered");
        var user = database.findUser(userId);
    </script>
</onentry>

Custom Logging

java
machine.setLogger(msg -> System.out.println("[SM] " + msg));

Datamodel Configuration

java
machine.configureDataModel(dm -> {
    dm.set("config", loadConfiguration());
    dm.set("services", serviceRegistry);
});

Native-Image Support

For GraalVM native-image compilation:

1. Use GraalJS

java
// At startup
DataModelFactory.setOverride(DataModelType.ECMASCRIPT, DataModelType.GRAALJS);

2. Use StaticInvokeFactory

java
// Avoid runtime SCXML compilation
machine.setInvokeFactory(new StaticInvokeFactory(Map.of(
    "child1.scxml", Child1Machine::new,
    "child2.scxml", Child2Machine::new
)));

3. Register for Reflection

Create reflect-config.json:

json
[
  {
    "name": "com.example.MyStateMachine",
    "allDeclaredMethods": true,
    "allDeclaredFields": true
  }
]

API Reference

TranspiledStateMachine

java
public abstract class TranspiledStateMachine {
    // Lifecycle
    void start();
    void start(DataModelType type, Map<String, Object> initData);
    void initialize();
    boolean isFinished();

    // Events (string-based)
    void send(String name, Object data);
    void processDelayedEvents();

    // Events (integer-based - recommended)
    void send(int eventId);
    boolean send(int eventId, Object payload);

    // State inspection
    Set<String> getActiveStateIds();
    boolean isInState(String stateId);

    // Configuration
    void setLogger(Consumer<String> logger);
    void setInvokeFactory(InvokeFactory factory);
    void configureDataModel(Consumer<DataModel> configurer);
    DataModel getDataModel();
}

ContinuousStateMachineExecutor

java
public class ContinuousStateMachineExecutor implements AutoCloseable {
    ContinuousStateMachineExecutor(TranspiledStateMachine machine);

    void start();
    void close();

    CompletableFuture<Void> sendAsync(String name, Object data);
    CompletableFuture<Void> submitAsync(Runnable task);
}

DataModelFactory

java
public final class DataModelFactory {
    static void setOverride(DataModelType requested, DataModelType actual);
    static void setSupplier(DataModelType type, Supplier<DataModel> supplier);
    static void setSupplier(String name, Supplier<DataModel> supplier);
    static void resetConfiguration();
}

Event Introspection

java
Set<String> all = machine.getAllEvents();              // all transition events
Set<String> enabled = machine.getEnabledEvents();      // guard-aware enabled events
Set<String> forState = machine.getEventsForState("s1"); // events state s1 handles
Set<String> enabledFor = machine.getEnabledEventsForState("s1"); // guard-aware, one state

Guard evaluation: In() predicates are compiled to O(1) bitset checks. Native-java expressions use direct evaluation. ECMAScript/XPath expressions are evaluated via the datamodel engine.


Troubleshooting

Delayed events not firing

Symptom: <send delay="..."> events never arrive.

Solution: Ensure executor is started:

java
ContinuousStateMachineExecutor executor =
    new ContinuousStateMachineExecutor(machine);
executor.start();  // Required!

"No datamodel implementation" error

Symptom: DataModelException: No implementation for ecmascript

Solution: Add Rhino or GraalJS dependency:

gradle
implementation 'org.mozilla:rhino:1.7.15'

Thread safety issues

Symptom: Inconsistent state, concurrent modification errors.

Solution: Use ContinuousStateMachineExecutor which handles thread safety internally. Don't call send() from multiple threads without an executor.

GraalVM native-image fails

Symptom: Reflection errors at runtime.

Solution:

  1. Use GraalJS instead of Rhino
  2. Use StaticInvokeFactory
  3. Add reflection configuration

See Also

Target Guides

Reference

Tutorials