Java Target Guide
Complete guide for generating and running SCXML state machines on the JVM.
Table of Contents
- Overview
- Quick Start
- Code Generation
- Dependencies
- Datamodel Support
- Executor Patterns
- Event Processing
- Invoke Children
- Configuration Reference
- Native-Image Support
- API Reference
- 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
scxml-gen traffic.scxml -o TrafficLight.java --package com.example
2. Add Dependencies
dependencies {
implementation 'eu.mihosoft.scxml:scxml-core:0.1.0-SNAPSHOT'
implementation 'org.mozilla:rhino:1.7.15' // ECMAScript support
}
3. Use in Application
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
# 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:
# 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
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
<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.
<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
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.
<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.
<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).
<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)
// 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)
// Synchronous execution for deterministic testing
RunToCompletionStateMachineExecutor executor =
new RunToCompletionStateMachineExecutor(machine);
executor.start();
// Events processed immediately, synchronously on the caller's thread
executor.send(EVT_STEP1); // Completes before returning
executor.send(EVT_STEP2); // Deterministic order
// Manual delayed event processing (drain matured timers)
while (!machine.isFinished()) {
executor.pumpEvents();
Thread.sleep(10);
}
executor.close();
Always go through the executor. The
TranspiledStateMachineexposes asend(...)method too, but calling it directly bypasses the executor's serialization and is not safe when timer threads (or other threads) are running. Reserve directmachine.send(...)calls for the simplest use cases where no executor exists.
Tracing
Full tracing support for debugging, analysis, and state machine monitoring.
JsonlTraceWriter (JSONL Output)
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
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
Event ID Constants (Recommended)
Generated constants provide O(1) event dispatch:
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
// 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
// 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:
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):
// Default behavior - compiles SCXML at runtime
InvokeFactory.setDefault(new RuntimeInvokeFactory());
Configuration Reference
Java Object Binding
Register Java objects for use in ECMAScript:
// Initialize datamodel first
machine.initialize();
// Register objects before start
machine.getDataModel().set("logger", new MyLogger());
machine.getDataModel().set("database", databaseService);
// Then start
executor.start();
<!-- Access in SCXML -->
<onentry>
<script>
logger.info("State entered");
var user = database.findUser(userId);
</script>
</onentry>
Custom Logging
machine.setLogger(msg -> System.out.println("[SM] " + msg));
Datamodel Configuration
machine.configureDataModel(dm -> {
dm.set("config", loadConfiguration());
dm.set("services", serviceRegistry);
});
Native-Image Support
For GraalVM native-image compilation:
1. Use GraalJS
// At startup
DataModelFactory.setOverride(DataModelType.ECMASCRIPT, DataModelType.GRAALJS);
2. Use StaticInvokeFactory
// 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:
[
{
"name": "com.example.MyStateMachine",
"allDeclaredMethods": true,
"allDeclaredFields": true
}
]
API Reference
TranspiledStateMachine
public abstract class TranspiledStateMachine implements ScxmlStateMachine {
// Lifecycle
void start();
void start(Map<String, Object> initData);
void start(DataModelType type, Map<String, Object> initData);
void initialize();
boolean isFinished();
boolean isStarted();
// Events (string-based) - prefer the executor instead
void send(String name);
boolean send(String name, Object data);
void send(Event event);
// Events (integer-based - O(1) dispatch)
void send(int eventId);
boolean send(int eventId, Object payload);
// Drain matured timers / external events (also exposed as
// executor.pumpEvents()); processDelayedEvents() is deprecated.
boolean pumpEvents();
// 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();
}
StateMachineExecutor (interface)
ContinuousStateMachineExecutor and RunToCompletionStateMachineExecutor
both implement this interface, so production code and tests share a single
API.
public interface StateMachineExecutor extends AutoCloseable {
// Lifecycle
void start();
void start(Map<String, Object> initData);
void start(DataModelType type);
void start(DataModelType type, Map<String, Object> initData);
boolean isRunning();
void close();
// Synchronous events (block until the macrostep completes)
void send(String eventName);
void send(Event event);
void send(int eventId); // O(1) dispatch
void send(int eventId, Object payload);
// Async events
CompletableFuture<Void> sendAsync(String eventName);
CompletableFuture<Void> sendAsync(Event event);
CompletableFuture<Void> sendAsync(int eventId);
CompletableFuture<Void> sendAsync(int eventId, Object payload);
// Process matured timers / external events on the executor's thread
boolean pumpEvents();
ScxmlStateMachine getStateMachine();
}
ContinuousStateMachineExecutor
public final class ContinuousStateMachineExecutor implements StateMachineExecutor {
ContinuousStateMachineExecutor(ScxmlStateMachine machine);
// StateMachineExecutor methods (start, send, sendAsync, pumpEvents, close, ...)
// Continuous-only conveniences
void waitForCompletion() throws InterruptedException;
boolean waitForCompletion(long timeout, TimeUnit unit) throws InterruptedException;
void onStateChange(BiConsumer<Set<String>, Set<String>> listener);
}
DataModelFactory
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
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:
ContinuousStateMachineExecutor executor =
new ContinuousStateMachineExecutor(machine);
executor.start(); // Required!
"No datamodel implementation" error
Symptom: DataModelException: No implementation for ecmascript
Solution: Add Rhino or GraalJS dependency:
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:
- Use GraalJS instead of Rhino
- Use StaticInvokeFactory
- Add reflection configuration
See Also
Target Guides
- JavaScript Target - Node.js, Browser, React/Vue
- C Target - Embedded, Arduino, MISRA
Reference
- Datamodels Guide - Detailed datamodel comparison
- Feature Matrix - Supported SCXML elements
- W3C Compliance - Test results
Tutorials
- User Guide - Complete toolchain guide
- Tutorial - Step-by-step introduction