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
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)
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 {
// 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
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
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