forks work
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -35,3 +35,10 @@ out/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### added
|
||||||
|
*.png
|
||||||
|
*.dot
|
||||||
|
*.scxml.xml
|
||||||
116
src/main/java/com/example/statemachinedemo/AstStateUtils.java
Normal file
116
src/main/java/com/example/statemachinedemo/AstStateUtils.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,148 +3,172 @@ package com.example.statemachinedemo;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class ExportUtils {
|
public class ExportUtils {
|
||||||
|
|
||||||
public static String generatePlantUML(List<Transition> transitions,
|
public static String generatePlantUML(List<Transition> transitions,
|
||||||
Set<String> startStates,
|
Set<String> startStates,
|
||||||
Set<String> endStates,
|
Set<String> endStates,
|
||||||
boolean useLambdaGuards) {
|
boolean useLambdaGuards) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("@startuml\n");
|
sb.append("@startuml\n");
|
||||||
sb.append("skinparam state {\n");
|
sb.append("skinparam state {\n");
|
||||||
sb.append(" BackgroundColor<<choice>> LightYellow\n");
|
sb.append(" BackgroundColor<<choice>> LightYellow\n");
|
||||||
sb.append("}\n\n");
|
sb.append("}\n\n");
|
||||||
|
|
||||||
// 1. Print start states
|
// 1. Print start states
|
||||||
for (String start : startStates) {
|
for (String start : startStates) {
|
||||||
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
||||||
}
|
}
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
// 2. Find all states that have choice transitions originating from them
|
// 2. Find all states that have choice transitions originating from them
|
||||||
Set<String> statesWithChoice = new HashSet<>();
|
Set<String> statesWithChoice = new HashSet<>();
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if ("withChoice".equals(t.type)) {
|
if ("withChoice".equals(t.type)) {
|
||||||
statesWithChoice.add(simplify(t.sourceState));
|
for (String source : t.sourceStates) {
|
||||||
|
statesWithChoice.add(simplify(source));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Declare choice pseudostates & connect from original state
|
// 3. Declare choice pseudostates & connect from original state
|
||||||
for (String state : statesWithChoice) {
|
for (String state : statesWithChoice) {
|
||||||
String choiceState = state + "_choice";
|
String choiceState = state + "_choice";
|
||||||
sb.append("state ").append(choiceState).append(" <<choice>>\n");
|
sb.append("state ").append(choiceState).append(" <<choice>>\n");
|
||||||
sb.append(state).append(" --> ").append(choiceState).append("\n");
|
sb.append(state).append(" --> ").append(choiceState).append("\n");
|
||||||
}
|
}
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
// Palette of colors for choice states (NO leading '#')
|
// Palette of colors for choice states (NO leading '#')
|
||||||
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
||||||
|
|
||||||
// Assign a color per withChoice source state cycling through palette
|
// Assign a color per withChoice source state cycling through palette
|
||||||
Map<String, String> choiceStateColorMap = new HashMap<>();
|
Map<String, String> choiceStateColorMap = new HashMap<>();
|
||||||
int colorIndex = 0;
|
int colorIndex = 0;
|
||||||
for (String state : statesWithChoice) {
|
for (String state : statesWithChoice) {
|
||||||
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
|
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
|
||||||
colorIndex++;
|
colorIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> transitionLines = new HashSet<>();
|
Set<String> transitionLines = new HashSet<>();
|
||||||
|
|
||||||
// Helper to get color for non-choice transitions (NO leading '#')
|
// Helper to get color for non-choice transitions (NO leading '#')
|
||||||
java.util.function.Function<String, String> getTransitionColor = (type) -> {
|
java.util.function.Function<String, String> getTransitionColor = (type) -> {
|
||||||
if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue
|
if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue
|
||||||
if ("withInternal".equals(type)) return "32CD32"; // LimeGreen
|
if ("withInternal".equals(type)) return "32CD32"; // LimeGreen
|
||||||
if ("withJunction".equals(type)) return "FF69B4"; // HotPink
|
if ("withJunction".equals(type)) return "FF69B4"; // HotPink
|
||||||
return "000000"; // Black fallback
|
return "000000"; // Black fallback
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Output transitions
|
// 4. Output transitions
|
||||||
for (Transition t : transitions) {
|
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);
|
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
||||||
String target = simplify(t.targetState);
|
|
||||||
|
|
||||||
String transitionSource = source;
|
List<String> targets;
|
||||||
|
if (t.targetStates == null || t.targetStates.isEmpty()) {
|
||||||
// Reroute choice transitions through pseudostate
|
if (!allowNoTarget) {
|
||||||
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
// Use sourceStates as targets (self-loop)
|
||||||
transitionSource = source + "_choice";
|
targets = t.sourceStates;
|
||||||
|
} else {
|
||||||
|
// withJoin or withFork with no targets => skip emitting transitions
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
targets = t.targetStates;
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare guard text
|
for (String rawSource : t.sourceStates) {
|
||||||
String guardText = "";
|
String source = simplify(rawSource);
|
||||||
if (t.guard != null && !t.guard.isBlank()) {
|
|
||||||
if (useLambdaGuards && t.isLambdaGuard) {
|
for (String rawTarget : targets) {
|
||||||
guardText = "lambda";
|
String target = simplify(rawTarget);
|
||||||
} else {
|
|
||||||
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
String transitionSource = source;
|
||||||
|
|
||||||
|
// Reroute choice transitions through pseudostate
|
||||||
|
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
||||||
|
transitionSource = source + "_choice";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare actions text (comma separated)
|
// Prepare guard text
|
||||||
String actionsText = "";
|
String guardText = "";
|
||||||
if (t.actions != null && !t.actions.isEmpty()) {
|
if (t.guard != null && !t.guard.isBlank()) {
|
||||||
StringBuilder actionBuilder = new StringBuilder();
|
if (useLambdaGuards && t.isLambdaGuard) {
|
||||||
for (int i = 0; i < t.actions.size(); i++) {
|
guardText = "lambda";
|
||||||
if (i > 0) actionBuilder.append(", ");
|
|
||||||
String action = t.actions.get(i);
|
|
||||||
boolean isLambda = t.isLambdaActions.get(i);
|
|
||||||
if (useLambdaGuards && isLambda) {
|
|
||||||
actionBuilder.append("lambda");
|
|
||||||
} else {
|
} else {
|
||||||
actionBuilder.append(action);
|
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
actionsText = actionBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build label with event, guard, and actions
|
// Prepare actions text (comma separated)
|
||||||
StringBuilder label = new StringBuilder();
|
String actionsText = "";
|
||||||
if (t.event != null && !t.event.isBlank()) {
|
if (t.actions != null && !t.actions.isEmpty()) {
|
||||||
label.append(simplify(t.event));
|
StringBuilder actionBuilder = new StringBuilder();
|
||||||
}
|
for (int i = 0; i < t.actions.size(); i++) {
|
||||||
if (!guardText.isBlank()) {
|
if (i > 0) actionBuilder.append(", ");
|
||||||
if (label.length() > 0) label.append(" ");
|
String action = t.actions.get(i);
|
||||||
label.append("[").append(guardText).append("]");
|
boolean isLambda = t.isLambdaActions.get(i);
|
||||||
}
|
if (useLambdaGuards && isLambda) {
|
||||||
if (!actionsText.isBlank()) {
|
actionBuilder.append("lambda");
|
||||||
if (label.length() > 0) label.append(" / ");
|
} else {
|
||||||
label.append(actionsText);
|
actionBuilder.append(action);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
actionsText = actionBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// Add order info for choice/junction transitions
|
// Build label with event, guard, and actions
|
||||||
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
StringBuilder label = new StringBuilder();
|
||||||
if (label.length() > 0) label.append(" ");
|
if (t.event != null && !t.event.isBlank()) {
|
||||||
label.append("(order=").append(t.order).append(")");
|
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
|
// Add order info for choice/junction transitions
|
||||||
String color;
|
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
||||||
if ("withChoice".equals(t.type)) {
|
if (label.length() > 0) label.append(" ");
|
||||||
color = choiceStateColorMap.getOrDefault(source, "000000");
|
label.append("(order=").append(t.order).append(")");
|
||||||
} else {
|
}
|
||||||
color = getTransitionColor.apply(t.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add color with single '#' prefix
|
// Determine color for this transition
|
||||||
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
|
String color;
|
||||||
|
if ("withChoice".equals(t.type)) {
|
||||||
|
color = choiceStateColorMap.getOrDefault(source, "000000");
|
||||||
|
} else {
|
||||||
|
color = getTransitionColor.apply(t.type);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip duplicates
|
// Add color with single '#' prefix
|
||||||
if (transitionLines.add(line)) {
|
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
|
||||||
sb.append(line).append("\n");
|
|
||||||
|
// 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,
|
public static String generateDot(List<Transition> transitions,
|
||||||
Set<String> startStates,
|
Set<String> startStates,
|
||||||
Set<String> endStates,
|
Set<String> endStates,
|
||||||
@@ -161,15 +185,21 @@ public class ExportUtils {
|
|||||||
Set<String> statesWithChoice = new HashSet<>();
|
Set<String> statesWithChoice = new HashSet<>();
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if ("withChoice".equals(t.type)) {
|
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
|
// 3. Declare normal states and choice pseudostates
|
||||||
Set<String> allStates = new HashSet<>();
|
Set<String> allStates = new HashSet<>();
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
allStates.add(simplify(t.sourceState));
|
if (t.sourceStates != null) {
|
||||||
allStates.add(simplify(t.targetState));
|
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)
|
// Print all normal states (except choice pseudostates for now)
|
||||||
@@ -205,66 +235,86 @@ public class ExportUtils {
|
|||||||
|
|
||||||
// 6. Transitions: reroute withChoice transitions via choice pseudostate
|
// 6. Transitions: reroute withChoice transitions via choice pseudostate
|
||||||
for (Transition t : transitions) {
|
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);
|
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
||||||
String target = simplify(t.targetState);
|
|
||||||
String edgeSource = source;
|
|
||||||
|
|
||||||
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
List<String> targets;
|
||||||
edgeSource = source + "_choice";
|
if (t.targetStates == null || t.targetStates.isEmpty()) {
|
||||||
}
|
if (!allowNoTarget) {
|
||||||
|
// self-loop: targets = sources
|
||||||
// Prepare guard text
|
targets = t.sourceStates;
|
||||||
String guardText = "";
|
|
||||||
if (t.guard != null && !t.guard.isBlank()) {
|
|
||||||
if (useLambdaGuards && t.isLambdaGuard) {
|
|
||||||
guardText = "lambda";
|
|
||||||
} else {
|
} 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
|
for (String rawSource : t.sourceStates) {
|
||||||
String actionsText = "";
|
String source = simplify(rawSource);
|
||||||
if (t.actions != null && !t.actions.isEmpty()) {
|
|
||||||
StringBuilder actionBuilder = new StringBuilder();
|
for (String rawTarget : targets) {
|
||||||
for (int i = 0; i < t.actions.size(); i++) {
|
String target = simplify(rawTarget);
|
||||||
if (i > 0) actionBuilder.append(", ");
|
String edgeSource = source;
|
||||||
String action = t.actions.get(i);
|
|
||||||
boolean isLambda = t.isLambdaActions.get(i);
|
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
||||||
if (useLambdaGuards && isLambda) {
|
edgeSource = source + "_choice";
|
||||||
actionBuilder.append("lambda");
|
|
||||||
} else {
|
|
||||||
actionBuilder.append(action);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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");
|
sb.append("}\n");
|
||||||
@@ -282,8 +332,12 @@ public class ExportUtils {
|
|||||||
// 1. Collect all states
|
// 1. Collect all states
|
||||||
Set<String> allStates = new HashSet<>();
|
Set<String> allStates = new HashSet<>();
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (t.sourceState != null) allStates.add(t.sourceState);
|
if (t.sourceStates != null) {
|
||||||
if (t.targetState != null) allStates.add(t.targetState);
|
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
|
// 2. Output <state> elements with transitions
|
||||||
@@ -300,8 +354,25 @@ public class ExportUtils {
|
|||||||
|
|
||||||
// Add transitions from this state
|
// Add transitions from this state
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (state.equals(t.sourceState)) {
|
if (t.sourceStates == null || !t.sourceStates.contains(state)) continue;
|
||||||
String target = simplify(t.targetState);
|
|
||||||
|
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;
|
if (target == null || target.isEmpty()) continue;
|
||||||
|
|
||||||
String event = t.event != null ? simplify(t.event) : null;
|
String event = t.event != null ? simplify(t.event) : null;
|
||||||
@@ -319,7 +390,7 @@ public class ExportUtils {
|
|||||||
sb.append(" target=\"").append(target).append("\"");
|
sb.append(" target=\"").append(target).append("\"");
|
||||||
if (guard != null && !guard.isBlank()) sb.append(" cond=\"").append(escapeXml(guard)).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) {
|
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
||||||
sb.append(">\n");
|
sb.append(">\n");
|
||||||
sb.append(" <!-- order=").append(t.order).append(" -->\n");
|
sb.append(" <!-- order=").append(t.order).append(" -->\n");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.statemachinedemo;
|
package com.example.statemachinedemo;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -7,6 +8,7 @@ import java.io.PrintWriter;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -44,22 +46,40 @@ public class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find start and end states
|
// Find start and end states
|
||||||
Set<String> startStates = StateUtils.findStartStates(transitions);
|
Set<String> startStatesTransitions = StateUtils.findStartStates(transitions);
|
||||||
Set<String> endStates = StateUtils.findEndStates(transitions);
|
Set<String> endStatesTransitions = StateUtils.findEndStates(transitions);
|
||||||
System.out.println("Start States: " + startStates);
|
|
||||||
System.out.println("End States: " + endStates);
|
|
||||||
|
|
||||||
// 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);
|
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);
|
out.println(plantUML);
|
||||||
}
|
}
|
||||||
|
|
||||||
String dot = generateDot(transitions, startStates, endStates, true);
|
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);
|
out.println(dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
String scxml = exportToSCXML(transitions, startStates, endStates, true);
|
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);
|
out.println(scxml);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.example.statemachinedemo;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class StateUtils {
|
public class StateUtils {
|
||||||
|
|
||||||
public static Set<String> findStartStates(List<Transition> transitions) {
|
public static Set<String> findStartStates(List<Transition> transitions) {
|
||||||
@@ -10,16 +11,23 @@ public class StateUtils {
|
|||||||
Set<String> targets = new HashSet<>();
|
Set<String> targets = new HashSet<>();
|
||||||
|
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
String source = stripQuotes(t.sourceState);
|
// Add all source states after stripping quotes
|
||||||
String target = stripQuotes(t.targetState);
|
for (String sourceState : t.sourceStates) {
|
||||||
|
String source = stripQuotes(sourceState);
|
||||||
|
if (source != null) sources.add(source);
|
||||||
|
}
|
||||||
|
|
||||||
if (source != null) sources.add(source);
|
// Add all target states after stripping quotes,
|
||||||
if (target != null && !target.equals(source)) {
|
// but only if not equal to any of the sources of this transition (to allow self-targets)
|
||||||
targets.add(target);
|
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);
|
sources.removeAll(targets);
|
||||||
return sources;
|
return sources;
|
||||||
}
|
}
|
||||||
@@ -29,16 +37,22 @@ public class StateUtils {
|
|||||||
Set<String> sources = new HashSet<>();
|
Set<String> sources = new HashSet<>();
|
||||||
|
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
String source = stripQuotes(t.sourceState);
|
// Add all target states
|
||||||
String target = stripQuotes(t.targetState);
|
for (String targetState : t.targetStates) {
|
||||||
|
String target = stripQuotes(targetState);
|
||||||
|
if (target != null) targets.add(target);
|
||||||
|
}
|
||||||
|
|
||||||
if (target != null) targets.add(target);
|
// Add all source states, but exclude ones equal to any target of this transition (self-source allowed)
|
||||||
if (source != null && !source.equals(target)) {
|
for (String sourceState : t.sourceStates) {
|
||||||
sources.add(source);
|
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);
|
targets.removeAll(sources);
|
||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import java.util.List;
|
|||||||
@ToString
|
@ToString
|
||||||
public class Transition {
|
public class Transition {
|
||||||
public String type; // withExternal, withChoice, withInternal, etc.
|
public String type; // withExternal, withChoice, withInternal, etc.
|
||||||
public String sourceState;
|
public List<String> sourceStates = new ArrayList<>();
|
||||||
public String targetState;
|
public List<String> targetStates = new ArrayList<>();
|
||||||
public String event;
|
public String event;
|
||||||
|
|
||||||
public String guard;
|
public String guard;
|
||||||
@@ -22,4 +22,5 @@ public class Transition {
|
|||||||
public List<Boolean> isLambdaActions = new ArrayList<>();
|
public List<Boolean> isLambdaActions = new ArrayList<>();
|
||||||
|
|
||||||
public Integer order = null; // optional order for withChoice or withJunction
|
public Integer order = null; // optional order for withChoice or withJunction
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -96,8 +96,8 @@ public class TransitionParser {
|
|||||||
|
|
||||||
Transition t = new Transition();
|
Transition t = new Transition();
|
||||||
t.type = type;
|
t.type = type;
|
||||||
t.sourceState = sourceState;
|
t.sourceStates.add(sourceState);
|
||||||
t.targetState = args.get(0).toString();
|
t.targetStates.add(args.get(0).toString());
|
||||||
t.event = null;
|
t.event = null;
|
||||||
|
|
||||||
// guard is optional second argument
|
// guard is optional second argument
|
||||||
@@ -160,15 +160,23 @@ public class TransitionParser {
|
|||||||
|
|
||||||
if (SUPPORTED_TRANSITION_TYPES.contains(name)) {
|
if (SUPPORTED_TRANSITION_TYPES.contains(name)) {
|
||||||
t.type = name;
|
t.type = name;
|
||||||
continue; // Skip the rest of the switch for this iteration
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "source":
|
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;
|
break;
|
||||||
case "target":
|
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;
|
break;
|
||||||
case "event":
|
case "event":
|
||||||
if (!args.isEmpty()) t.event = args.get(0).toString();
|
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;
|
return t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package com.example.statemachinedemo;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
@SpringBootTest
|
//@SpringBootTest
|
||||||
class StatemachinedemoApplicationTests {
|
class StatemachinedemoApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user