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 ### ### VS Code ###
.vscode/ .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,6 +3,7 @@ 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,
@@ -23,7 +24,9 @@ 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));
}
} }
} }
@@ -58,10 +61,28 @@ public class ExportUtils {
// 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);
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;
}
for (String rawSource : t.sourceStates) {
String source = simplify(rawSource);
for (String rawTarget : targets) {
String target = simplify(rawTarget);
String transitionSource = source; String transitionSource = source;
@@ -133,6 +154,8 @@ public class ExportUtils {
sb.append(line).append("\n"); sb.append(line).append("\n");
} }
} }
}
}
sb.append("\n"); sb.append("\n");
@@ -145,6 +168,7 @@ public class ExportUtils {
return sb.toString(); 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,10 +235,28 @@ 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);
List<String> targets;
if (t.targetStates == null || t.targetStates.isEmpty()) {
if (!allowNoTarget) {
// self-loop: targets = sources
targets = t.sourceStates;
} else {
// withJoin/withFork with no targets: skip
continue;
}
} else {
targets = t.targetStates;
}
for (String rawSource : t.sourceStates) {
String source = simplify(rawSource);
for (String rawTarget : targets) {
String target = simplify(rawTarget);
String edgeSource = source; String edgeSource = source;
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) { if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
@@ -266,6 +314,8 @@ public class ExportUtils {
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n"); sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
} }
}
}
sb.append("}\n"); sb.append("}\n");
return sb.toString(); return sb.toString();
@@ -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");

View File

@@ -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);
} }
} }

View File

@@ -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);
if (target != null && !target.equals(source)) { }
// 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); 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);
if (source != null && !source.equals(target)) { }
// 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); 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;
} }

View File

@@ -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
} }

View File

@@ -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;
} }
} }

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.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