forks work

This commit is contained in:
2025-07-13 13:10:18 +02:00
parent 5de15f8a68
commit f984f31a24
10 changed files with 703 additions and 205 deletions

7
.gitignore vendored
View File

@@ -35,3 +35,10 @@ out/
### VS Code ###
.vscode/
#### added
*.png
*.dot
*.scxml.xml

View File

@@ -0,0 +1,116 @@
package com.example.statemachinedemo;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class AstStateUtils {
public static class StateConfigurerVisitor extends ASTVisitor {
private Set<String> initialStates = new HashSet<>();
private Set<String> endStates = new HashSet<>();
public Set<String> getInitialStates() {
return initialStates;
}
public Set<String> getEndStates() {
return endStates;
}
@Override
public boolean visit(MethodDeclaration node) {
// Visit method named "configure" only
if (!"configure".equals(node.getName().getIdentifier())) {
return super.visit(node);
}
Block body = node.getBody();
if (body == null) return false;
// Traverse statements inside configure method
for (Object stmtObj : body.statements()) {
if (!(stmtObj instanceof ExpressionStatement)) continue;
ExpressionStatement exprStmt = (ExpressionStatement) stmtObj;
Expression expr = exprStmt.getExpression();
if (expr instanceof MethodInvocation) {
MethodInvocation rootCall = (MethodInvocation) expr;
// Check if this chain starts with "withStates"
if (isWithStatesChain(rootCall)) {
parseMethodChain(rootCall);
}
}
}
return false; // no need to visit further inside configure
}
private boolean isWithStatesChain(MethodInvocation mi) {
MethodInvocation current = mi;
while (current != null) {
if ("withStates".equals(current.getName().getIdentifier())) {
return true;
}
Expression expr = current.getExpression();
if (expr instanceof MethodInvocation) {
current = (MethodInvocation) expr;
} else {
break;
}
}
return false;
}
private boolean isInsideRegionOrFork(MethodInvocation mi) {
Expression expr = mi.getExpression();
while (expr instanceof MethodInvocation) {
MethodInvocation parent = (MethodInvocation) expr;
String name = parent.getName().getIdentifier();
if ("fork".equals(name) || "region".equals(name)) {
return true;
}
expr = parent.getExpression();
}
return false;
}
private void parseMethodChain(MethodInvocation mi) {
List<MethodInvocation> chain = new ArrayList<>();
Expression current = mi;
while (current instanceof MethodInvocation) {
MethodInvocation m = (MethodInvocation) current;
chain.add(0, m);
current = m.getExpression();
}
for (MethodInvocation call : chain) {
String methodName = call.getName().getIdentifier();
List<?> args = call.arguments();
if (args.isEmpty()) continue;
if ("initial".equals(methodName)) {
if (!isInsideRegionOrFork(call)) {
Expression arg = (Expression) args.get(0);
initialStates.add(arg.toString());
}
} else if ("end".equals(methodName)) {
Expression arg = (Expression) args.get(0);
endStates.add(arg.toString());
}
// ignore other calls for extraction
}
}
}
public static CompilationUnit parse(String source) {
ASTParser parser = ASTParser.newParser(AST.JLS21);
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
return (CompilationUnit) parser.createAST(null);
}
}

View File

@@ -3,148 +3,172 @@ package com.example.statemachinedemo;
import java.util.*;
public class ExportUtils {
public static String generatePlantUML(List<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards) {
StringBuilder sb = new StringBuilder();
sb.append("@startuml\n");
sb.append("skinparam state {\n");
sb.append(" BackgroundColor<<choice>> LightYellow\n");
sb.append("}\n\n");
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards) {
StringBuilder sb = new StringBuilder();
sb.append("@startuml\n");
sb.append("skinparam state {\n");
sb.append(" BackgroundColor<<choice>> LightYellow\n");
sb.append("}\n\n");
// 1. Print start states
for (String start : startStates) {
sb.append("[*] --> ").append(simplify(start)).append("\n");
}
sb.append("\n");
// 1. Print start states
for (String start : startStates) {
sb.append("[*] --> ").append(simplify(start)).append("\n");
}
sb.append("\n");
// 2. Find all states that have choice transitions originating from them
Set<String> statesWithChoice = new HashSet<>();
for (Transition t : transitions) {
if ("withChoice".equals(t.type)) {
statesWithChoice.add(simplify(t.sourceState));
// 2. Find all states that have choice transitions originating from them
Set<String> statesWithChoice = new HashSet<>();
for (Transition t : transitions) {
if ("withChoice".equals(t.type)) {
for (String source : t.sourceStates) {
statesWithChoice.add(simplify(source));
}
}
}
// 3. Declare choice pseudostates & connect from original state
for (String state : statesWithChoice) {
String choiceState = state + "_choice";
sb.append("state ").append(choiceState).append(" <<choice>>\n");
sb.append(state).append(" --> ").append(choiceState).append("\n");
}
sb.append("\n");
// 3. Declare choice pseudostates & connect from original state
for (String state : statesWithChoice) {
String choiceState = state + "_choice";
sb.append("state ").append(choiceState).append(" <<choice>>\n");
sb.append(state).append(" --> ").append(choiceState).append("\n");
}
sb.append("\n");
// Palette of colors for choice states (NO leading '#')
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
// Palette of colors for choice states (NO leading '#')
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
// Assign a color per withChoice source state cycling through palette
Map<String, String> choiceStateColorMap = new HashMap<>();
int colorIndex = 0;
for (String state : statesWithChoice) {
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
colorIndex++;
}
// Assign a color per withChoice source state cycling through palette
Map<String, String> choiceStateColorMap = new HashMap<>();
int colorIndex = 0;
for (String state : statesWithChoice) {
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
colorIndex++;
}
Set<String> transitionLines = new HashSet<>();
Set<String> transitionLines = new HashSet<>();
// Helper to get color for non-choice transitions (NO leading '#')
java.util.function.Function<String, String> getTransitionColor = (type) -> {
if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue
if ("withInternal".equals(type)) return "32CD32"; // LimeGreen
if ("withJunction".equals(type)) return "FF69B4"; // HotPink
return "000000"; // Black fallback
};
// Helper to get color for non-choice transitions (NO leading '#')
java.util.function.Function<String, String> getTransitionColor = (type) -> {
if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue
if ("withInternal".equals(type)) return "32CD32"; // LimeGreen
if ("withJunction".equals(type)) return "FF69B4"; // HotPink
return "000000"; // Black fallback
};
// 4. Output transitions
for (Transition t : transitions) {
if (t.sourceState == null || t.targetState == null) continue;
// 4. Output transitions
for (Transition t : transitions) {
if (t.sourceStates == null || t.sourceStates.isEmpty()) continue;
String source = simplify(t.sourceState);
String target = simplify(t.targetState);
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
String transitionSource = source;
// Reroute choice transitions through pseudostate
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
transitionSource = source + "_choice";
List<String> targets;
if (t.targetStates == null || t.targetStates.isEmpty()) {
if (!allowNoTarget) {
// Use sourceStates as targets (self-loop)
targets = t.sourceStates;
} else {
// withJoin or withFork with no targets => skip emitting transitions
continue;
}
} else {
targets = t.targetStates;
}
// Prepare guard text
String guardText = "";
if (t.guard != null && !t.guard.isBlank()) {
if (useLambdaGuards && t.isLambdaGuard) {
guardText = "lambda";
} else {
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
for (String rawSource : t.sourceStates) {
String source = simplify(rawSource);
for (String rawTarget : targets) {
String target = simplify(rawTarget);
String transitionSource = source;
// Reroute choice transitions through pseudostate
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
transitionSource = source + "_choice";
}
}
// Prepare actions text (comma separated)
String actionsText = "";
if (t.actions != null && !t.actions.isEmpty()) {
StringBuilder actionBuilder = new StringBuilder();
for (int i = 0; i < t.actions.size(); i++) {
if (i > 0) actionBuilder.append(", ");
String action = t.actions.get(i);
boolean isLambda = t.isLambdaActions.get(i);
if (useLambdaGuards && isLambda) {
actionBuilder.append("lambda");
// Prepare guard text
String guardText = "";
if (t.guard != null && !t.guard.isBlank()) {
if (useLambdaGuards && t.isLambdaGuard) {
guardText = "lambda";
} else {
actionBuilder.append(action);
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
}
}
actionsText = actionBuilder.toString();
}
// Build label with event, guard, and actions
StringBuilder label = new StringBuilder();
if (t.event != null && !t.event.isBlank()) {
label.append(simplify(t.event));
}
if (!guardText.isBlank()) {
if (label.length() > 0) label.append(" ");
label.append("[").append(guardText).append("]");
}
if (!actionsText.isBlank()) {
if (label.length() > 0) label.append(" / ");
label.append(actionsText);
}
// Prepare actions text (comma separated)
String actionsText = "";
if (t.actions != null && !t.actions.isEmpty()) {
StringBuilder actionBuilder = new StringBuilder();
for (int i = 0; i < t.actions.size(); i++) {
if (i > 0) actionBuilder.append(", ");
String action = t.actions.get(i);
boolean isLambda = t.isLambdaActions.get(i);
if (useLambdaGuards && isLambda) {
actionBuilder.append("lambda");
} else {
actionBuilder.append(action);
}
}
actionsText = actionBuilder.toString();
}
// Add order info for choice/junction transitions
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
if (label.length() > 0) label.append(" ");
label.append("(order=").append(t.order).append(")");
}
// Build label with event, guard, and actions
StringBuilder label = new StringBuilder();
if (t.event != null && !t.event.isBlank()) {
label.append(simplify(t.event));
}
if (!guardText.isBlank()) {
if (label.length() > 0) label.append(" ");
label.append("[").append(guardText).append("]");
}
if (!actionsText.isBlank()) {
if (label.length() > 0) label.append(" / ");
label.append(actionsText);
}
// Determine color for this transition
String color;
if ("withChoice".equals(t.type)) {
color = choiceStateColorMap.getOrDefault(source, "000000");
} else {
color = getTransitionColor.apply(t.type);
}
// Add order info for choice/junction transitions
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
if (label.length() > 0) label.append(" ");
label.append("(order=").append(t.order).append(")");
}
// Add color with single '#' prefix
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
// Determine color for this transition
String color;
if ("withChoice".equals(t.type)) {
color = choiceStateColorMap.getOrDefault(source, "000000");
} else {
color = getTransitionColor.apply(t.type);
}
// Skip duplicates
if (transitionLines.add(line)) {
sb.append(line).append("\n");
// Add color with single '#' prefix
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
// Skip duplicates
if (transitionLines.add(line)) {
sb.append(line).append("\n");
}
}
}
sb.append("\n");
// 5. Mark end states with transition to [*]
for (String end : endStates) {
sb.append(simplify(end)).append(" --> [*]\n");
}
sb.append("@enduml\n");
return sb.toString();
}
sb.append("\n");
// 5. Mark end states with transition to [*]
for (String end : endStates) {
sb.append(simplify(end)).append(" --> [*]\n");
}
sb.append("@enduml\n");
return sb.toString();
}
public static String generateDot(List<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
@@ -161,15 +185,21 @@ public class ExportUtils {
Set<String> statesWithChoice = new HashSet<>();
for (Transition t : transitions) {
if ("withChoice".equals(t.type)) {
statesWithChoice.add(simplify(t.sourceState));
for (String source : t.sourceStates) {
statesWithChoice.add(simplify(source));
}
}
}
// 3. Declare normal states and choice pseudostates
Set<String> allStates = new HashSet<>();
for (Transition t : transitions) {
allStates.add(simplify(t.sourceState));
allStates.add(simplify(t.targetState));
if (t.sourceStates != null) {
for (String s : t.sourceStates) allStates.add(simplify(s));
}
if (t.targetStates != null) {
for (String tgt : t.targetStates) allStates.add(simplify(tgt));
}
}
// Print all normal states (except choice pseudostates for now)
@@ -205,66 +235,86 @@ public class ExportUtils {
// 6. Transitions: reroute withChoice transitions via choice pseudostate
for (Transition t : transitions) {
if (t.sourceState == null || t.targetState == null) continue;
if (t.sourceStates == null || t.sourceStates.isEmpty()) continue;
String source = simplify(t.sourceState);
String target = simplify(t.targetState);
String edgeSource = source;
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
edgeSource = source + "_choice";
}
// Prepare guard text
String guardText = "";
if (t.guard != null && !t.guard.isBlank()) {
if (useLambdaGuards && t.isLambdaGuard) {
guardText = "lambda";
List<String> targets;
if (t.targetStates == null || t.targetStates.isEmpty()) {
if (!allowNoTarget) {
// self-loop: targets = sources
targets = t.sourceStates;
} else {
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
// withJoin/withFork with no targets: skip
continue;
}
} else {
targets = t.targetStates;
}
// Prepare actions text
String actionsText = "";
if (t.actions != null && !t.actions.isEmpty()) {
StringBuilder actionBuilder = new StringBuilder();
for (int i = 0; i < t.actions.size(); i++) {
if (i > 0) actionBuilder.append(", ");
String action = t.actions.get(i);
boolean isLambda = t.isLambdaActions.get(i);
if (useLambdaGuards && isLambda) {
actionBuilder.append("lambda");
} else {
actionBuilder.append(action);
for (String rawSource : t.sourceStates) {
String source = simplify(rawSource);
for (String rawTarget : targets) {
String target = simplify(rawTarget);
String edgeSource = source;
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
edgeSource = source + "_choice";
}
// Prepare guard text
String guardText = "";
if (t.guard != null && !t.guard.isBlank()) {
if (useLambdaGuards && t.isLambdaGuard) {
guardText = "lambda";
} else {
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
}
}
// Prepare actions text
String actionsText = "";
if (t.actions != null && !t.actions.isEmpty()) {
StringBuilder actionBuilder = new StringBuilder();
for (int i = 0; i < t.actions.size(); i++) {
if (i > 0) actionBuilder.append(", ");
String action = t.actions.get(i);
boolean isLambda = t.isLambdaActions.get(i);
if (useLambdaGuards && isLambda) {
actionBuilder.append("lambda");
} else {
actionBuilder.append(action);
}
}
actionsText = actionBuilder.toString();
}
// Build label with event, guard, and actions
StringBuilder label = new StringBuilder();
if (t.event != null && !t.event.isBlank()) {
label.append(simplify(t.event));
}
if (!guardText.isBlank()) {
if (label.length() > 0) label.append(" ");
label.append("[").append(guardText).append("]");
}
if (!actionsText.isBlank()) {
if (label.length() > 0) label.append(" / ");
label.append(actionsText);
}
// Add order info for choice/junction transitions
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
if (label.length() > 0) label.append(" ");
label.append("(order=").append(t.order).append(")");
}
String edgeLabel = label.length() > 0 ? " [label=\"" + label.toString() + "\"]" : "";
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
}
actionsText = actionBuilder.toString();
}
// Build label with event, guard, and actions
StringBuilder label = new StringBuilder();
if (t.event != null && !t.event.isBlank()) {
label.append(simplify(t.event));
}
if (!guardText.isBlank()) {
if (label.length() > 0) label.append(" ");
label.append("[").append(guardText).append("]");
}
if (!actionsText.isBlank()) {
if (label.length() > 0) label.append(" / ");
label.append(actionsText);
}
// Add order info for choice/junction transitions
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
if (label.length() > 0) label.append(" ");
label.append("(order=").append(t.order).append(")");
}
String edgeLabel = label.length() > 0 ? " [label=\"" + label.toString() + "\"]" : "";
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
}
sb.append("}\n");
@@ -282,8 +332,12 @@ public class ExportUtils {
// 1. Collect all states
Set<String> allStates = new HashSet<>();
for (Transition t : transitions) {
if (t.sourceState != null) allStates.add(t.sourceState);
if (t.targetState != null) allStates.add(t.targetState);
if (t.sourceStates != null) {
for (String s : t.sourceStates) allStates.add(s);
}
if (t.targetStates != null) {
for (String tState : t.targetStates) allStates.add(tState);
}
}
// 2. Output <state> elements with transitions
@@ -300,8 +354,25 @@ public class ExportUtils {
// Add transitions from this state
for (Transition t : transitions) {
if (state.equals(t.sourceState)) {
String target = simplify(t.targetState);
if (t.sourceStates == null || !t.sourceStates.contains(state)) continue;
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
List<String> targets;
if (t.targetStates == null || t.targetStates.isEmpty()) {
if (!allowNoTarget) {
// self-loop target = source
targets = List.of(state);
} else {
// withJoin/withFork with no target: skip
continue;
}
} else {
targets = t.targetStates;
}
for (String rawTarget : targets) {
String target = simplify(rawTarget);
if (target == null || target.isEmpty()) continue;
String event = t.event != null ? simplify(t.event) : null;
@@ -319,7 +390,7 @@ public class ExportUtils {
sb.append(" target=\"").append(target).append("\"");
if (guard != null && !guard.isBlank()) sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
// If choice/junction and order is set, add XML comment inside transition
// Add order info for choice/junction transitions as XML comment inside transition
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
sb.append(">\n");
sb.append(" <!-- order=").append(t.order).append(" -->\n");

View File

@@ -1,5 +1,6 @@
package com.example.statemachinedemo;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import java.io.IOException;
@@ -7,6 +8,7 @@ import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -44,22 +46,40 @@ public class Main {
}
// Find start and end states
Set<String> startStates = StateUtils.findStartStates(transitions);
Set<String> endStates = StateUtils.findEndStates(transitions);
System.out.println("Start States: " + startStates);
System.out.println("End States: " + endStates);
Set<String> startStatesTransitions = StateUtils.findStartStates(transitions);
Set<String> endStatesTransitions = StateUtils.findEndStates(transitions);
// Export outputs
CompilationUnit cu = AstStateUtils.parse(source);
AstStateUtils.StateConfigurerVisitor visitor = new AstStateUtils.StateConfigurerVisitor();
cu.accept(visitor);
System.out.println("Start States Ast: " + visitor.getInitialStates());
System.out.println("End States Ast: " + visitor.getEndStates());
System.out.println("Start States: " + startStatesTransitions);
System.out.println("End States: " + endStatesTransitions);
Set<String> startStates = new HashSet<>(startStatesTransitions); // copy set1
startStates.addAll(visitor.getInitialStates()); // add all from set2
Set<String> endStates = new HashSet<>(endStatesTransitions); // copy set1
endStates.addAll(visitor.getEndStates()); // add all from set2
// Derive output base name from input file name
String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", "");
// Export outputs using dynamic file names
String plantUML = generatePlantUML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.2.dot")) {
try (PrintWriter out = new PrintWriter(baseName + ".plantuml.dot")) {
out.println(plantUML);
}
String dot = generateDot(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.1.dot")) {
try (PrintWriter out = new PrintWriter(baseName + ".dot")) {
out.println(dot);
}
String scxml = exportToSCXML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.3.xml")) {
try (PrintWriter out = new PrintWriter(baseName + ".scxml.xml")) {
out.println(scxml);
}
}

View File

@@ -3,6 +3,7 @@ package com.example.statemachinedemo;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class StateUtils {
public static Set<String> findStartStates(List<Transition> transitions) {
@@ -10,16 +11,23 @@ public class StateUtils {
Set<String> targets = new HashSet<>();
for (Transition t : transitions) {
String source = stripQuotes(t.sourceState);
String target = stripQuotes(t.targetState);
// Add all source states after stripping quotes
for (String sourceState : t.sourceStates) {
String source = stripQuotes(sourceState);
if (source != null) sources.add(source);
}
if (source != null) sources.add(source);
if (target != null && !target.equals(source)) {
targets.add(target);
// Add all target states after stripping quotes,
// but only if not equal to any of the sources of this transition (to allow self-targets)
for (String targetState : t.targetStates) {
String target = stripQuotes(targetState);
if (target != null && !t.sourceStates.stream().map(StateUtils::stripQuotes).anyMatch(s -> s.equals(target))) {
targets.add(target);
}
}
}
// Start states = sources that are not a target (except self-targets allowed)
// Start states = sources that are not targets
sources.removeAll(targets);
return sources;
}
@@ -29,16 +37,22 @@ public class StateUtils {
Set<String> sources = new HashSet<>();
for (Transition t : transitions) {
String source = stripQuotes(t.sourceState);
String target = stripQuotes(t.targetState);
// Add all target states
for (String targetState : t.targetStates) {
String target = stripQuotes(targetState);
if (target != null) targets.add(target);
}
if (target != null) targets.add(target);
if (source != null && !source.equals(target)) {
sources.add(source);
// Add all source states, but exclude ones equal to any target of this transition (self-source allowed)
for (String sourceState : t.sourceStates) {
String source = stripQuotes(sourceState);
if (source != null && !t.targetStates.stream().map(StateUtils::stripQuotes).anyMatch(tgt -> tgt.equals(source))) {
sources.add(source);
}
}
}
// End states = targets that are not a source (except self-source allowed)
// End states = targets that are not sources
targets.removeAll(sources);
return targets;
}

View File

@@ -11,8 +11,8 @@ import java.util.List;
@ToString
public class Transition {
public String type; // withExternal, withChoice, withInternal, etc.
public String sourceState;
public String targetState;
public List<String> sourceStates = new ArrayList<>();
public List<String> targetStates = new ArrayList<>();
public String event;
public String guard;
@@ -22,4 +22,5 @@ public class Transition {
public List<Boolean> isLambdaActions = new ArrayList<>();
public Integer order = null; // optional order for withChoice or withJunction
}

View File

@@ -96,8 +96,8 @@ public class TransitionParser {
Transition t = new Transition();
t.type = type;
t.sourceState = sourceState;
t.targetState = args.get(0).toString();
t.sourceStates.add(sourceState);
t.targetStates.add(args.get(0).toString());
t.event = null;
// guard is optional second argument
@@ -160,15 +160,23 @@ public class TransitionParser {
if (SUPPORTED_TRANSITION_TYPES.contains(name)) {
t.type = name;
continue; // Skip the rest of the switch for this iteration
continue;
}
switch (name) {
case "source":
if (!args.isEmpty()) t.sourceState = args.get(0).toString();
// Collect all source arguments (for withJoin typically)
for (Object argObj : args) {
Expression argExpr = (Expression) argObj;
t.sourceStates.add(argExpr.toString());
}
break;
case "target":
if (!args.isEmpty()) t.targetState = args.get(0).toString();
// Collect all target arguments (for withFork typically)
for (Object argObj : args) {
Expression argExpr = (Expression) argObj;
t.targetStates.add(argExpr.toString());
}
break;
case "event":
if (!args.isEmpty()) t.event = args.get(0).toString();
@@ -190,11 +198,6 @@ public class TransitionParser {
}
}
// Handle self-transition
if (t.sourceState != null && t.targetState == null) {
t.targetState = t.sourceState;
}
return t;
}
}

View File

@@ -0,0 +1,163 @@
package com.example.statemachinedemo.statemachine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
import java.util.EnumSet;
@Configuration
@EnableStateMachineFactory
public class ComplexStateMachineConfig extends StateMachineConfigurerAdapter<ComplexStateMachineConfig.States, ComplexStateMachineConfig.Events> {
public enum States {
STATE1, STATE2, STATE3, STATE4, STATE5,
STATE6, STATE7, STATE8, STATE9, STATE10,
STATE11, STATE12, STATE13, STATE14, STATE15,
STATE16, STATE17, STATE18, STATE19, STATE20
}
public enum Events {
EVENT1, EVENT2, EVENT3, EVENT4, EVENT5,
EVENT6, EVENT7, EVENT8, EVENT9, EVENT10,
EVENT11, EVENT12, EVENT13, EVENT14, EVENT15
}
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.STATE1)
.states(EnumSet.allOf(States.class))
.stateEntry(States.STATE5, logEntryAction("Entered STATE5"))
.stateExit(States.STATE10, logExitAction("Exited STATE10"));
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
// External transitions
transitions
.withExternal().source(States.STATE1).target(States.STATE2).event(Events.EVENT1)
.and()
.withExternal().source(States.STATE2).target(States.STATE3).event(Events.EVENT2)
.and()
.withExternal().source(States.STATE3).target(States.STATE4).event(Events.EVENT3)
.and()
.withExternal().source(States.STATE4).target(States.STATE5).event(Events.EVENT4)
.and()
.withExternal().source(States.STATE5).target(States.STATE6).event(Events.EVENT5)
.and()
.withExternal().source(States.STATE6).target(States.STATE7).event(Events.EVENT6)
.and()
.withExternal().source(States.STATE7).target(States.STATE8).event(Events.EVENT7)
.and()
.withExternal().source(States.STATE8).target(States.STATE9).event(Events.EVENT8)
.and()
.withExternal().source(States.STATE9).target(States.STATE10).event(Events.EVENT9)
.and()
.withExternal().source(States.STATE10).target(States.STATE11).event(Events.EVENT10)
.and()
.withExternal().source(States.STATE11).target(States.STATE12).event(Events.EVENT11)
.and()
.withExternal().source(States.STATE12).target(States.STATE13).event(Events.EVENT12)
.and()
.withExternal().source(States.STATE13).target(States.STATE14).event(Events.EVENT13)
.and()
.withExternal().source(States.STATE14).target(States.STATE15).event(Events.EVENT14)
.and()
.withExternal().source(States.STATE15).target(States.STATE16).event(Events.EVENT15);
// Choice transitions (at least 10)
transitions
.withChoice()
.source(States.STATE16)
.first(States.STATE17, guardVarEquals("value1"))
.then(States.STATE18, guardVarEquals("value2"))
.last(States.STATE19)
.and()
.withChoice()
.source(States.STATE17)
.first(States.STATE20, guardEventHeaderEquals("header1", "foo"))
.last(States.STATE16)
.and()
.withChoice()
.source(States.STATE18)
.first(States.STATE19, guardVarEquals("value3"))
.last(States.STATE20)
.and()
.withChoice()
.source(States.STATE19)
.first(States.STATE1, guardVarEquals("reset"))
.last(States.STATE20)
.and()
.withChoice()
.source(States.STATE20)
.first(States.STATE5, guardEventHeaderEquals("header2", "bar"))
.last(States.STATE1)
.and()
.withChoice()
.source(States.STATE11)
.first(States.STATE13, guardVarEquals("goTo13"))
.then(States.STATE14, guardVarEquals("goTo14"))
.last(States.STATE15)
.and()
.withChoice()
.source(States.STATE12)
.first(States.STATE10, guardVarEquals("goBack10"))
.last(States.STATE11)
.and()
.withChoice()
.source(States.STATE14)
.first(States.STATE12, guardVarEquals("loop12"))
.last(States.STATE16)
.and()
.withChoice()
.source(States.STATE9)
.first(States.STATE8, guardVarEquals("stepBack"))
.then(States.STATE7, guardVarEquals("stepBackMore"))
.last(States.STATE6)
.and()
.withChoice()
.source(States.STATE8)
.first(States.STATE9, guardVarEquals("forward9"))
.last(States.STATE10);
}
private Guard<States, Events> guardVarEquals(String expected) {
return context -> {
Object var = context.getExtendedState().getVariables().get("var");
return expected.equals(var);
};
}
private Guard<States, Events> guardEventHeaderEquals(String headerName, String expected) {
return context -> {
Object header = context.getMessageHeader(headerName);
return expected.equals(header);
};
}
private Action<States, Events> logEntryAction(String msg) {
return context -> System.out.println(msg);
}
private Action<States, Events> logExitAction(String msg) {
return context -> System.out.println(msg);
}
}

View File

@@ -0,0 +1,103 @@
package com.example.statemachinedemo.statemachine;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.state.EnumState;
import org.springframework.statemachine.guard.Guard;
import java.util.EnumSet;
@Configuration
@EnableStateMachine
public class ForkJoinStateMachineConfig extends StateMachineConfigurerAdapter<ForkJoinStateMachineConfig.States, ForkJoinStateMachineConfig.Events> {
public enum States {
// Main flow
START, FORK, JOIN, END,
// Region 1
REGION1_STATE1, REGION1_STATE2,
// Region 2
REGION2_STATE1, REGION2_STATE2
}
public enum Events {
TO_FORK, R1_NEXT, R2_NEXT, TO_JOIN, TO_END
}
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.START)
.state(States.FORK)
.fork(States.FORK)
.join(States.JOIN)
.end(States.END)
.and()
.withStates()
.parent(States.FORK)
.region("Region1")
.initial(States.REGION1_STATE1)
.state(States.REGION1_STATE2)
.and()
.withStates()
.parent(States.FORK)
.region("Region2")
.initial(States.REGION2_STATE1)
.state(States.REGION2_STATE2);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
// Initial path to fork
.withExternal()
.source(States.START).target(States.FORK).event(Events.TO_FORK)
.and()
// Fork: START → Region1 & Region2
.withFork()
.source(States.FORK)
.target(States.REGION1_STATE1)
.target(States.REGION2_STATE1)
.and()
// Region1 flow
.withExternal()
.source(States.REGION1_STATE1).target(States.REGION1_STATE2).event(Events.R1_NEXT)
.and()
// Region2 flow
.withExternal()
.source(States.REGION2_STATE1).target(States.REGION2_STATE2).event(Events.R2_NEXT)
.and()
// Join when both regions reach REGIONx_STATE2
.withJoin()
.source(States.REGION1_STATE2)
.source(States.REGION2_STATE2)
.target(States.JOIN)
.and()
// From join to end
.withExternal()
.source(States.JOIN).target(States.END).event(Events.TO_END);
}
private Action<States, Events> log(String message) {
return context -> System.out.println("ACTION: " + message);
}
private Guard<States, Events> alwaysTrue() {
return context -> true;
}
}

View File

@@ -3,7 +3,7 @@ package com.example.statemachinedemo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
//@SpringBootTest
class StatemachinedemoApplicationTests {
@Test