diff --git a/.gitignore b/.gitignore index c2065bc..7566808 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,10 @@ out/ ### VS Code ### .vscode/ + + + +#### added +*.png +*.dot +*.scxml.xml \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/AstStateUtils.java b/src/main/java/com/example/statemachinedemo/AstStateUtils.java new file mode 100644 index 0000000..e91fa43 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/AstStateUtils.java @@ -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 initialStates = new HashSet<>(); + private Set endStates = new HashSet<>(); + + public Set getInitialStates() { + return initialStates; + } + + public Set 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 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); + } +} diff --git a/src/main/java/com/example/statemachinedemo/ExportUtils.java b/src/main/java/com/example/statemachinedemo/ExportUtils.java index db1a33e..5e0a1b6 100644 --- a/src/main/java/com/example/statemachinedemo/ExportUtils.java +++ b/src/main/java/com/example/statemachinedemo/ExportUtils.java @@ -3,148 +3,172 @@ package com.example.statemachinedemo; import java.util.*; public class ExportUtils { + public static String generatePlantUML(List transitions, - Set startStates, - Set endStates, - boolean useLambdaGuards) { - StringBuilder sb = new StringBuilder(); - sb.append("@startuml\n"); - sb.append("skinparam state {\n"); - sb.append(" BackgroundColor<> LightYellow\n"); - sb.append("}\n\n"); + Set startStates, + Set endStates, + boolean useLambdaGuards) { + StringBuilder sb = new StringBuilder(); + sb.append("@startuml\n"); + sb.append("skinparam state {\n"); + sb.append(" BackgroundColor<> 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 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 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(" <>\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(" <>\n"); + sb.append(state).append(" --> ").append(choiceState).append("\n"); + } + sb.append("\n"); - // Palette of colors for choice states (NO leading '#') - List choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4"); + // Palette of colors for choice states (NO leading '#') + List choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4"); - // Assign a color per withChoice source state cycling through palette - Map 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 choiceStateColorMap = new HashMap<>(); + int colorIndex = 0; + for (String state : statesWithChoice) { + choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size())); + colorIndex++; + } - Set transitionLines = new HashSet<>(); + Set transitionLines = new HashSet<>(); - // Helper to get color for non-choice transitions (NO leading '#') - java.util.function.Function 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 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 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 transitions, Set startStates, Set endStates, @@ -161,15 +185,21 @@ public class ExportUtils { Set 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 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 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 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 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 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(" \n"); diff --git a/src/main/java/com/example/statemachinedemo/Main.java b/src/main/java/com/example/statemachinedemo/Main.java index 050053d..ebb5fa1 100644 --- a/src/main/java/com/example/statemachinedemo/Main.java +++ b/src/main/java/com/example/statemachinedemo/Main.java @@ -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 startStates = StateUtils.findStartStates(transitions); - Set endStates = StateUtils.findEndStates(transitions); - System.out.println("Start States: " + startStates); - System.out.println("End States: " + endStates); + Set startStatesTransitions = StateUtils.findStartStates(transitions); + Set 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 startStates = new HashSet<>(startStatesTransitions); // copy set1 + startStates.addAll(visitor.getInitialStates()); // add all from set2 + Set 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); } } diff --git a/src/main/java/com/example/statemachinedemo/StateUtils.java b/src/main/java/com/example/statemachinedemo/StateUtils.java index 77d0e9e..7869ef8 100644 --- a/src/main/java/com/example/statemachinedemo/StateUtils.java +++ b/src/main/java/com/example/statemachinedemo/StateUtils.java @@ -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 findStartStates(List transitions) { @@ -10,16 +11,23 @@ public class StateUtils { Set 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 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; } diff --git a/src/main/java/com/example/statemachinedemo/Transition.java b/src/main/java/com/example/statemachinedemo/Transition.java index c4c6931..6b4a884 100644 --- a/src/main/java/com/example/statemachinedemo/Transition.java +++ b/src/main/java/com/example/statemachinedemo/Transition.java @@ -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 sourceStates = new ArrayList<>(); + public List targetStates = new ArrayList<>(); public String event; public String guard; @@ -22,4 +22,5 @@ public class Transition { public List isLambdaActions = new ArrayList<>(); public Integer order = null; // optional order for withChoice or withJunction + } \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/TransitionParser.java b/src/main/java/com/example/statemachinedemo/TransitionParser.java index 1243d66..16fda5e 100644 --- a/src/main/java/com/example/statemachinedemo/TransitionParser.java +++ b/src/main/java/com/example/statemachinedemo/TransitionParser.java @@ -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; } } diff --git a/src/main/java/com/example/statemachinedemo/statemachine/ComplexStateMachineConfig.java b/src/main/java/com/example/statemachinedemo/statemachine/ComplexStateMachineConfig.java new file mode 100644 index 0000000..ff58919 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/statemachine/ComplexStateMachineConfig.java @@ -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 { + + 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 config) throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer 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 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 guardVarEquals(String expected) { + return context -> { + Object var = context.getExtendedState().getVariables().get("var"); + return expected.equals(var); + }; + } + + private Guard guardEventHeaderEquals(String headerName, String expected) { + return context -> { + Object header = context.getMessageHeader(headerName); + return expected.equals(header); + }; + } + + private Action logEntryAction(String msg) { + return context -> System.out.println(msg); + } + + private Action logExitAction(String msg) { + return context -> System.out.println(msg); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/statemachine/ForkJoinStateMachineConfig.java b/src/main/java/com/example/statemachinedemo/statemachine/ForkJoinStateMachineConfig.java new file mode 100644 index 0000000..f0a8f83 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/statemachine/ForkJoinStateMachineConfig.java @@ -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 { + + 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 config) throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer 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 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 log(String message) { + return context -> System.out.println("ACTION: " + message); + } + + private Guard alwaysTrue() { + return context -> true; + } +} diff --git a/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java b/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java index 570c043..41ff069 100644 --- a/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java +++ b/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java @@ -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