refactoring
This commit is contained in:
@@ -1,50 +0,0 @@
|
|||||||
package com.example.statemachinedemo;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.file.*;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public class AstFileFinder {
|
|
||||||
|
|
||||||
public static List<Path> findJavaFiles(Path startDir, String extension) throws IOException {
|
|
||||||
try (Stream<Path> paths = Files.walk(startDir)) {
|
|
||||||
return paths
|
|
||||||
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasClassWithAnnotation(String source, String annotationName) {
|
|
||||||
ASTParser parser = ASTParser.newParser(AST.JLS17);
|
|
||||||
parser.setSource(source.toCharArray());
|
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
|
||||||
parser.setResolveBindings(false);
|
|
||||||
|
|
||||||
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
|
|
||||||
|
|
||||||
final boolean[] found = {false};
|
|
||||||
|
|
||||||
cu.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(TypeDeclaration node) {
|
|
||||||
if (node.isInterface()) return true;
|
|
||||||
|
|
||||||
for (Object modifier : node.modifiers()) {
|
|
||||||
if (modifier instanceof Annotation) {
|
|
||||||
Annotation annotation = (Annotation) modifier;
|
|
||||||
String name = annotation.getTypeName().getFullyQualifiedName();
|
|
||||||
if (name.equals(annotationName)) {
|
|
||||||
found[0] = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return found[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
package com.example.statemachinedemo;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class ExportUtils {
|
|
||||||
|
|
||||||
public static String generatePlantUML(List<Transition> transitions,
|
|
||||||
Set<String> startStates,
|
|
||||||
Set<String> endStates,
|
|
||||||
boolean useLambdaGuards) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("@startuml\n");
|
|
||||||
sb.append("skinparam state {\n");
|
|
||||||
sb.append(" BackgroundColor<<choice>> LightYellow\n");
|
|
||||||
sb.append("}\n\n");
|
|
||||||
|
|
||||||
// 1. Print start states
|
|
||||||
for (String start : startStates) {
|
|
||||||
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
|
|
||||||
// 2. Find all states that have choice transitions originating from them
|
|
||||||
Set<String> statesWithChoice = new HashSet<>();
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
if ("withChoice".equals(t.type)) {
|
|
||||||
for (String source : t.sourceStates) {
|
|
||||||
statesWithChoice.add(simplify(source));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Declare choice pseudostates & connect from original state
|
|
||||||
for (String state : statesWithChoice) {
|
|
||||||
String choiceState = state + "_choice";
|
|
||||||
sb.append("state ").append(choiceState).append(" <<choice>>\n");
|
|
||||||
sb.append(state).append(" --> ").append(choiceState).append("\n");
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
|
|
||||||
// Palette of colors for choice states (NO leading '#')
|
|
||||||
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
|
||||||
|
|
||||||
// Assign a color per withChoice source state cycling through palette
|
|
||||||
Map<String, String> choiceStateColorMap = new HashMap<>();
|
|
||||||
int colorIndex = 0;
|
|
||||||
for (String state : statesWithChoice) {
|
|
||||||
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
|
|
||||||
colorIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> transitionLines = new HashSet<>();
|
|
||||||
|
|
||||||
// Helper to get color for non-choice transitions (NO leading '#')
|
|
||||||
java.util.function.Function<String, String> getTransitionColor = (type) -> switch (type) {
|
|
||||||
case "withExternal" -> "1E90FF"; // DodgerBlue
|
|
||||||
case "withInternal" -> "32CD32"; // LimeGreen
|
|
||||||
case "withLocal" -> "FFA500"; // Orange
|
|
||||||
case "withJunction" -> "FF69B4"; // HotPink
|
|
||||||
case "withJoin" -> "8A2BE2"; // BlueViolet
|
|
||||||
case "withFork" -> "20B2AA"; // LightSeaGreen
|
|
||||||
default -> "000000"; // Black fallback
|
|
||||||
};
|
|
||||||
// 4. Output transitions
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
if (t.sourceStates == null || t.sourceStates.isEmpty()) continue;
|
|
||||||
|
|
||||||
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Reroute choice transitions through pseudostate
|
|
||||||
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
|
||||||
transitionSource = 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 (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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(")");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine color for this transition
|
|
||||||
String color;
|
|
||||||
if ("withChoice".equals(t.type)) {
|
|
||||||
color = choiceStateColorMap.getOrDefault(source, "000000");
|
|
||||||
} else {
|
|
||||||
color = getTransitionColor.apply(t.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static String generateDot(List<Transition> transitions,
|
|
||||||
Set<String> startStates,
|
|
||||||
Set<String> endStates,
|
|
||||||
boolean useLambdaGuards) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("digraph stateMachine {\n");
|
|
||||||
sb.append(" rankdir=LR;\n");
|
|
||||||
sb.append(" node [shape=circle, style=filled, fillcolor=lightgray];\n\n");
|
|
||||||
|
|
||||||
// 1. Fake start node
|
|
||||||
sb.append(" start [shape=point, label=\"\"];\n");
|
|
||||||
|
|
||||||
// 2. Find states with choice transitions
|
|
||||||
Set<String> statesWithChoice = new HashSet<>();
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
if ("withChoice".equals(t.type)) {
|
|
||||||
for (String source : t.sourceStates) {
|
|
||||||
statesWithChoice.add(simplify(source));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Declare normal states and choice pseudostates
|
|
||||||
Set<String> allStates = new HashSet<>();
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
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)
|
|
||||||
for (String state : allStates) {
|
|
||||||
if (!statesWithChoice.contains(state + "_choice")) {
|
|
||||||
String shape = endStates.contains(state) ? "doublecircle" : "circle";
|
|
||||||
sb.append(" ").append(state)
|
|
||||||
.append(" [shape=").append(shape).append(", fillcolor=lightgray];\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
|
|
||||||
// Declare choice pseudostates as diamonds
|
|
||||||
for (String state : statesWithChoice) {
|
|
||||||
String choiceNode = state + "_choice";
|
|
||||||
sb.append(" ").append(choiceNode)
|
|
||||||
.append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n");
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
|
|
||||||
// 4. Connect start node(s) to start states
|
|
||||||
for (String start : startStates) {
|
|
||||||
sb.append(" start -> ").append(simplify(start)).append(";\n");
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
|
|
||||||
// 5. Connect normal states to their choice pseudostates
|
|
||||||
for (String state : statesWithChoice) {
|
|
||||||
String choiceNode = state + "_choice";
|
|
||||||
sb.append(" ").append(state).append(" -> ").append(choiceNode).append(";\n");
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
|
|
||||||
// 6. Transitions: reroute withChoice transitions via choice pseudostate
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
if (t.sourceStates == null || t.sourceStates.isEmpty()) continue;
|
|
||||||
|
|
||||||
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
|
||||||
|
|
||||||
List<String> targets;
|
|
||||||
if (t.targetStates == null || t.targetStates.isEmpty()) {
|
|
||||||
if (!allowNoTarget) {
|
|
||||||
// self-loop: 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;
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.append("}\n");
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String exportToSCXML(List<Transition> transitions,
|
|
||||||
Set<String> startStates,
|
|
||||||
Set<String> endStates,
|
|
||||||
boolean useLambdaGuards) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
|
||||||
sb.append("<scxml xmlns=\"http://www.w3.org/2005/07/scxml\" version=\"1.0\">\n\n");
|
|
||||||
|
|
||||||
// 1. Collect all states
|
|
||||||
Set<String> allStates = new HashSet<>();
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
if (t.sourceStates != null) {
|
|
||||||
for (String s : t.sourceStates) allStates.add(s);
|
|
||||||
}
|
|
||||||
if (t.targetStates != null) {
|
|
||||||
for (String tState : t.targetStates) allStates.add(tState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Output <state> elements with transitions
|
|
||||||
for (String state : allStates) {
|
|
||||||
String simpleState = simplify(state);
|
|
||||||
sb.append(" <state id=\"").append(simpleState).append("\">\n");
|
|
||||||
|
|
||||||
// If this state is a start state, add <initial> and transition to it
|
|
||||||
if (startStates.contains(state)) {
|
|
||||||
sb.append(" <initial>\n");
|
|
||||||
sb.append(" <transition target=\"").append(simpleState).append("\"/>\n");
|
|
||||||
sb.append(" </initial>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add transitions from this state
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
if (t.sourceStates == null || !t.sourceStates.contains(state)) continue;
|
|
||||||
|
|
||||||
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
|
||||||
|
|
||||||
List<String> targets;
|
|
||||||
if (t.targetStates == null || t.targetStates.isEmpty()) {
|
|
||||||
if (!allowNoTarget) {
|
|
||||||
// self-loop target = source
|
|
||||||
targets = List.of(state);
|
|
||||||
} else {
|
|
||||||
// withJoin/withFork with no target: skip
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
targets = t.targetStates;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String rawTarget : targets) {
|
|
||||||
String target = simplify(rawTarget);
|
|
||||||
if (target == null || target.isEmpty()) continue;
|
|
||||||
|
|
||||||
String event = t.event != null ? simplify(t.event) : null;
|
|
||||||
String guard = null;
|
|
||||||
if (t.guard != null && !t.guard.isBlank()) {
|
|
||||||
if (useLambdaGuards && t.isLambdaGuard) {
|
|
||||||
guard = "lambda";
|
|
||||||
} else {
|
|
||||||
guard = t.guard.replaceAll("[\\n\\r]+", " ").trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.append(" <transition");
|
|
||||||
if (event != null && !event.isBlank()) sb.append(" event=\"").append(event).append("\"");
|
|
||||||
sb.append(" target=\"").append(target).append("\"");
|
|
||||||
if (guard != null && !guard.isBlank()) sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
|
|
||||||
|
|
||||||
// Add order info for choice/junction transitions as XML comment inside transition
|
|
||||||
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
|
||||||
sb.append(">\n");
|
|
||||||
sb.append(" <!-- order=").append(t.order).append(" -->\n");
|
|
||||||
sb.append(" </transition>\n");
|
|
||||||
} else {
|
|
||||||
sb.append("/>\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.append(" </state>\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.append("</scxml>\n");
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: extract last enum part
|
|
||||||
private static String simplify(String full) {
|
|
||||||
if (full == null) return "";
|
|
||||||
int dot = full.lastIndexOf('.');
|
|
||||||
return dot >= 0 ? full.substring(dot + 1) : full;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String escapeXml(String s) {
|
|
||||||
if (s == null) return "";
|
|
||||||
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
||||||
.replace("\"", """).replace("'", "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
package com.example.statemachinedemo;
|
package com.example.statemachinedemo;
|
||||||
|
|
||||||
|
import com.example.statemachinedemo.app.AstTransitionParser;
|
||||||
|
import com.example.statemachinedemo.app.StateMachineStateConfigurationMethodFinder;
|
||||||
|
import com.example.statemachinedemo.app.StateMachineTransitionConfigurationMethodFinder;
|
||||||
|
import com.example.statemachinedemo.app.TransitionStateUtils;
|
||||||
|
import com.example.statemachinedemo.app.domain.Transition;
|
||||||
|
import com.example.statemachinedemo.common.FileUtils;
|
||||||
|
import com.example.statemachinedemo.in.AstFileFinder;
|
||||||
|
import com.example.statemachinedemo.out.Dot;
|
||||||
|
import com.example.statemachinedemo.out.PlantUml;
|
||||||
|
import com.example.statemachinedemo.out.Scxml;
|
||||||
|
import com.example.statemachinedemo.out.StateMachineExporter;
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
|
||||||
@@ -12,75 +23,66 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static com.example.statemachinedemo.AstFileFinder.hasClassWithAnnotation;
|
|
||||||
import static com.example.statemachinedemo.ExportUtils.*;
|
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
private static final String START_DIR = ".";
|
private static final String START_DIR = ".";
|
||||||
private static final String EXTENSION = ".java";
|
private static final String EXTENSION = ".java";
|
||||||
private static final String TARGET_ANNOTATION = "EnableStateMachineFactory";
|
private static final String TARGET_ANNOTATION = "EnableStateMachineFactory";
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
List<Path> javaFiles = AstFileFinder.findJavaFiles(Paths.get(START_DIR), EXTENSION);
|
List<Path> javaFiles = FileUtils.findFilesWithExtension(Paths.get(START_DIR), EXTENSION);
|
||||||
|
|
||||||
|
// List of output handlers
|
||||||
|
List<StateMachineExporter> outputs = List.of(
|
||||||
|
new PlantUml(),
|
||||||
|
new Dot(),
|
||||||
|
new Scxml()
|
||||||
|
);
|
||||||
|
|
||||||
for (Path javaFile : javaFiles) {
|
for (Path javaFile : javaFiles) {
|
||||||
String source = Files.readString(javaFile);
|
String source = Files.readString(javaFile);
|
||||||
|
AstFileFinder finder = new AstFileFinder(source);
|
||||||
|
|
||||||
// Check for annotation (optional)
|
if (finder.hasClassWithAnnotation(source, TARGET_ANNOTATION)) {
|
||||||
if (hasClassWithAnnotation(source, TARGET_ANNOTATION)) {
|
|
||||||
System.out.println("Found annotation in: " + javaFile.toAbsolutePath());
|
System.out.println("Found annotation in: " + javaFile.toAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find configure method using the reusable finder
|
MethodDeclaration configureMethod = new StateMachineTransitionConfigurationMethodFinder(source).findConfigureMethod();
|
||||||
MethodDeclaration configureMethod = StateMachineConfigureMethodFinder.findConfigureMethod(source);
|
|
||||||
if (configureMethod != null) {
|
if (configureMethod != null) {
|
||||||
System.out.println("Found configure method in: " + javaFile.toAbsolutePath());
|
System.out.println("Found configure method in: " + javaFile.toAbsolutePath());
|
||||||
|
|
||||||
// Print method body
|
|
||||||
System.out.println("Method body:\n" + configureMethod.getBody());
|
System.out.println("Method body:\n" + configureMethod.getBody());
|
||||||
|
|
||||||
// Parse transitions
|
List<Transition> transitions = AstTransitionParser.parseTransitions(configureMethod);
|
||||||
List<Transition> transitions = TransitionParser.parseTransitions(configureMethod);
|
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
System.out.println(t);
|
System.out.println(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find start and end states
|
Set<String> startStatesTransitions = TransitionStateUtils.findStartStates(transitions);
|
||||||
Set<String> startStatesTransitions = StateUtils.findStartStates(transitions);
|
Set<String> endStatesTransitions = TransitionStateUtils.findEndStates(transitions);
|
||||||
Set<String> endStatesTransitions = StateUtils.findEndStates(transitions);
|
|
||||||
|
|
||||||
|
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
|
||||||
CompilationUnit cu = AstStateUtils.parse(source);
|
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor = new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
|
||||||
AstStateUtils.StateConfigurerVisitor visitor = new AstStateUtils.StateConfigurerVisitor();
|
|
||||||
cu.accept(visitor);
|
cu.accept(visitor);
|
||||||
|
|
||||||
System.out.println("Start States Ast: " + visitor.getInitialStates());
|
System.out.println("Start States Ast: " + visitor.getInitialStates());
|
||||||
System.out.println("End States Ast: " + visitor.getEndStates());
|
System.out.println("End States Ast: " + visitor.getEndStates());
|
||||||
|
|
||||||
System.out.println("Start States: " + startStatesTransitions);
|
System.out.println("Start States: " + startStatesTransitions);
|
||||||
System.out.println("End States: " + endStatesTransitions);
|
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
|
|
||||||
|
|
||||||
|
Set<String> startStates = new HashSet<>(startStatesTransitions);
|
||||||
|
startStates.addAll(visitor.getInitialStates());
|
||||||
|
|
||||||
|
Set<String> endStates = new HashSet<>(endStatesTransitions);
|
||||||
|
endStates.addAll(visitor.getEndStates());
|
||||||
|
|
||||||
// Derive output base name from input file name
|
|
||||||
String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", "");
|
String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", "");
|
||||||
|
|
||||||
// Export outputs using dynamic file names
|
// Iterate over outputs and generate files dynamically
|
||||||
String plantUML = generatePlantUML(transitions, startStates, endStates, true);
|
for (StateMachineExporter output : outputs) {
|
||||||
try (PrintWriter out = new PrintWriter(baseName + ".plantuml.dot")) {
|
String content = output.export(transitions, startStates, endStates, true);
|
||||||
out.println(plantUML);
|
String fileName = baseName + output.getFileExtension();
|
||||||
|
try (PrintWriter out = new PrintWriter(fileName)) {
|
||||||
|
out.println(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
String dot = generateDot(transitions, startStates, endStates, true);
|
|
||||||
try (PrintWriter out = new PrintWriter(baseName + ".dot")) {
|
|
||||||
out.println(dot);
|
|
||||||
}
|
|
||||||
|
|
||||||
String scxml = exportToSCXML(transitions, startStates, endStates, true);
|
|
||||||
try (PrintWriter out = new PrintWriter(baseName + ".scxml.xml")) {
|
|
||||||
out.println(scxml);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
package com.example.statemachinedemo;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class StateMachineConfigureMethodFinder {
|
|
||||||
/**
|
|
||||||
* Parses source code and returns the MethodDeclaration for
|
|
||||||
* `public void configure(StateMachineTransitionConfigurer<T,D> transitions) throws Exception`
|
|
||||||
* in a class extending StateMachineConfigurerAdapter.
|
|
||||||
*
|
|
||||||
* @param source Java source code string
|
|
||||||
* @return MethodDeclaration of configure or null if not found
|
|
||||||
*/
|
|
||||||
public static MethodDeclaration findConfigureMethod(String source) {
|
|
||||||
ASTParser parser = ASTParser.newParser(AST.JLS17);
|
|
||||||
parser.setSource(source.toCharArray());
|
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
|
||||||
parser.setResolveBindings(false);
|
|
||||||
|
|
||||||
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
|
|
||||||
|
|
||||||
ConfigureMethodVisitor visitor = new ConfigureMethodVisitor();
|
|
||||||
cu.accept(visitor);
|
|
||||||
|
|
||||||
return visitor.getConfigureMethod();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ConfigureMethodVisitor extends ASTVisitor {
|
|
||||||
private MethodDeclaration configureMethod = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(TypeDeclaration node) {
|
|
||||||
// Check if class extends StateMachineConfigurerAdapter
|
|
||||||
if (!node.isInterface() && extendsStateMachineConfigurerAdapter(node)) {
|
|
||||||
// Look for configure method inside this class
|
|
||||||
for (MethodDeclaration method : node.getMethods()) {
|
|
||||||
if (isConfigureMethod(method)) {
|
|
||||||
configureMethod = method;
|
|
||||||
return false; // Found, stop visiting further
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration node) {
|
|
||||||
Type superclass = node.getSuperclassType();
|
|
||||||
if (superclass == null) return false;
|
|
||||||
|
|
||||||
// Superclass can be parameterized type
|
|
||||||
if (superclass.isParameterizedType()) {
|
|
||||||
ParameterizedType pt = (ParameterizedType) superclass;
|
|
||||||
String superName = pt.getType().toString();
|
|
||||||
return superName.equals("StateMachineConfigurerAdapter");
|
|
||||||
} else {
|
|
||||||
return superclass.toString().equals("StateMachineConfigurerAdapter");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConfigureMethod(MethodDeclaration method) {
|
|
||||||
if (!method.getName().getIdentifier().equals("configure")) return false;
|
|
||||||
|
|
||||||
// Must be public void
|
|
||||||
if (!Modifier.isPublic(method.getModifiers())) return false;
|
|
||||||
if (!(method.getReturnType2() != null && method.getReturnType2().isPrimitiveType())) return false;
|
|
||||||
if (!"void".equals(method.getReturnType2().toString())) return false;
|
|
||||||
|
|
||||||
// Exactly one parameter of type StateMachineTransitionConfigurer<T,D>
|
|
||||||
List<?> params = method.parameters();
|
|
||||||
if (params.size() != 1) return false;
|
|
||||||
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(0);
|
|
||||||
Type paramType = param.getType();
|
|
||||||
|
|
||||||
String paramTypeStr = paramType.toString();
|
|
||||||
|
|
||||||
// Since generic params, just check starts with StateMachineTransitionConfigurer
|
|
||||||
if (!paramTypeStr.startsWith("StateMachineTransitionConfigurer")) return false;
|
|
||||||
|
|
||||||
// Check throws Exception using thrownExceptionTypes()
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Type> thrownExceptions = method.thrownExceptionTypes();
|
|
||||||
boolean throwsExceptionFound = false;
|
|
||||||
for (Type excType : thrownExceptions) {
|
|
||||||
if (excType.toString().equals("Exception")) {
|
|
||||||
throwsExceptionFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!throwsExceptionFound) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MethodDeclaration getConfigureMethod() {
|
|
||||||
return configureMethod;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.example.statemachinedemo;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class StateUtils {
|
|
||||||
|
|
||||||
public static Set<String> findStartStates(List<Transition> transitions) {
|
|
||||||
Set<String> sources = new HashSet<>();
|
|
||||||
Set<String> targets = new HashSet<>();
|
|
||||||
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
// Add all source states after stripping quotes
|
|
||||||
for (String sourceState : t.sourceStates) {
|
|
||||||
String source = stripQuotes(sourceState);
|
|
||||||
if (source != null) sources.add(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start states = sources that are not targets
|
|
||||||
sources.removeAll(targets);
|
|
||||||
return sources;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<String> findEndStates(List<Transition> transitions) {
|
|
||||||
Set<String> targets = new HashSet<>();
|
|
||||||
Set<String> sources = new HashSet<>();
|
|
||||||
|
|
||||||
for (Transition t : transitions) {
|
|
||||||
// Add all target states
|
|
||||||
for (String targetState : t.targetStates) {
|
|
||||||
String target = stripQuotes(targetState);
|
|
||||||
if (target != null) targets.add(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End states = targets that are not sources
|
|
||||||
targets.removeAll(sources);
|
|
||||||
return targets;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripQuotes(String s) {
|
|
||||||
if (s == null) return null;
|
|
||||||
return s.replaceAll("^\"|\"$", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.example.statemachinedemo;
|
|
||||||
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses Spring State Machine transitions from configure method AST.
|
|
||||||
*/
|
|
||||||
@ToString
|
|
||||||
public class Transition {
|
|
||||||
public String type; // withExternal, withChoice, withInternal, etc.
|
|
||||||
public List<String> sourceStates = new ArrayList<>();
|
|
||||||
public List<String> targetStates = new ArrayList<>();
|
|
||||||
public String event;
|
|
||||||
|
|
||||||
public String guard;
|
|
||||||
public boolean isLambdaGuard = false;
|
|
||||||
|
|
||||||
public List<String> actions = new ArrayList<>();
|
|
||||||
public List<Boolean> isLambdaActions = new ArrayList<>();
|
|
||||||
|
|
||||||
public Integer order = null; // optional order for withChoice or withJunction
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
package com.example.statemachinedemo;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class TransitionParser {
|
|
||||||
|
|
||||||
public static List<Transition> parseTransitions(MethodDeclaration method) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
|
|
||||||
Block body = method.getBody();
|
|
||||||
if (body == null) return transitions;
|
|
||||||
|
|
||||||
for (Object stmtObj : body.statements()) {
|
|
||||||
if (!(stmtObj instanceof ExpressionStatement)) continue;
|
|
||||||
|
|
||||||
ExpressionStatement stmt = (ExpressionStatement) stmtObj;
|
|
||||||
Expression expr = stmt.getExpression();
|
|
||||||
if (!(expr instanceof MethodInvocation)) continue;
|
|
||||||
|
|
||||||
transitions.addAll(parseTransitionsFromExpression((MethodInvocation) expr));
|
|
||||||
}
|
|
||||||
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
|
|
||||||
// Step 1: Unroll method chain into ordered list
|
|
||||||
List<MethodInvocation> calls = new ArrayList<>();
|
|
||||||
Expression current = rootCall;
|
|
||||||
while (current instanceof MethodInvocation) {
|
|
||||||
MethodInvocation mi = (MethodInvocation) current;
|
|
||||||
calls.add(0, mi);
|
|
||||||
current = mi.getExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Split into segments by .and()
|
|
||||||
List<List<MethodInvocation>> segments = new ArrayList<>();
|
|
||||||
List<MethodInvocation> currentSegment = new ArrayList<>();
|
|
||||||
for (MethodInvocation call : calls) {
|
|
||||||
if ("and".equals(call.getName().getIdentifier())) {
|
|
||||||
if (!currentSegment.isEmpty()) {
|
|
||||||
segments.add(currentSegment);
|
|
||||||
currentSegment = new ArrayList<>();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentSegment.add(call);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!currentSegment.isEmpty()) {
|
|
||||||
segments.add(currentSegment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Parse each segment
|
|
||||||
for (List<MethodInvocation> segment : segments) {
|
|
||||||
boolean isChoice = segment.stream()
|
|
||||||
.anyMatch(mi -> "withChoice".equals(mi.getName().getIdentifier()));
|
|
||||||
|
|
||||||
boolean isJunction = segment.stream()
|
|
||||||
.anyMatch(mi -> "withJunction".equals(mi.getName().getIdentifier()));
|
|
||||||
|
|
||||||
if (isChoice || isJunction) {
|
|
||||||
transitions.addAll(parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction"));
|
|
||||||
} else {
|
|
||||||
transitions.add(parseStandardTransition(segment));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Transition> parseChoiceOrJunctionTransitions(List<MethodInvocation> segment, String type) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
String sourceState = null;
|
|
||||||
int orderCounter = 0;
|
|
||||||
|
|
||||||
for (MethodInvocation call : segment) {
|
|
||||||
String name = call.getName().getIdentifier();
|
|
||||||
|
|
||||||
if ("source".equals(name) && !call.arguments().isEmpty()) {
|
|
||||||
sourceState = call.arguments().get(0).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (MethodInvocation call : segment) {
|
|
||||||
String name = call.getName().getIdentifier();
|
|
||||||
List<?> args = call.arguments();
|
|
||||||
|
|
||||||
if ("first".equals(name) || "then".equals(name) || "last".equals(name)) {
|
|
||||||
if (args.isEmpty()) continue;
|
|
||||||
|
|
||||||
Transition t = new Transition();
|
|
||||||
t.type = type;
|
|
||||||
t.sourceStates.add(sourceState);
|
|
||||||
t.targetStates.add(args.get(0).toString());
|
|
||||||
t.event = null;
|
|
||||||
|
|
||||||
// guard is optional second argument
|
|
||||||
if (args.size() > 1) {
|
|
||||||
Expression guardExpr = (Expression) args.get(1);
|
|
||||||
t.guard = guardExpr.toString();
|
|
||||||
t.isLambdaGuard = isLambdaOrAnonymous(guardExpr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// action is optional third argument (only for .then())
|
|
||||||
if ("then".equals(name) && args.size() > 2) {
|
|
||||||
Expression actionExpr = (Expression) args.get(2);
|
|
||||||
t.actions.add(actionExpr.toString());
|
|
||||||
t.isLambdaActions.add(isLambdaOrAnonymous(actionExpr));
|
|
||||||
}
|
|
||||||
|
|
||||||
t.order = orderCounter++;
|
|
||||||
transitions.add(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isLambdaOrAnonymous(Expression expr) {
|
|
||||||
if (expr == null) return false;
|
|
||||||
|
|
||||||
// Lambda expressions
|
|
||||||
if (expr instanceof LambdaExpression) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anonymous class creation
|
|
||||||
if (expr instanceof ClassInstanceCreation) {
|
|
||||||
ClassInstanceCreation cic = (ClassInstanceCreation) expr;
|
|
||||||
if (cic.getAnonymousClassDeclaration() != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Set<String> SUPPORTED_TRANSITION_TYPES = Set.of(
|
|
||||||
"withExternal",
|
|
||||||
"withInternal",
|
|
||||||
"withLocal",
|
|
||||||
"withFork",
|
|
||||||
"withJoin",
|
|
||||||
"withChoice",
|
|
||||||
"withJunction"
|
|
||||||
);
|
|
||||||
|
|
||||||
private static Transition parseStandardTransition(List<MethodInvocation> segment) {
|
|
||||||
Transition t = new Transition();
|
|
||||||
|
|
||||||
for (MethodInvocation call : segment) {
|
|
||||||
String name = call.getName().getIdentifier();
|
|
||||||
List<?> args = call.arguments();
|
|
||||||
|
|
||||||
if (SUPPORTED_TRANSITION_TYPES.contains(name)) {
|
|
||||||
t.type = name;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (name) {
|
|
||||||
case "source":
|
|
||||||
// Collect all source arguments (for withJoin typically)
|
|
||||||
for (Object argObj : args) {
|
|
||||||
Expression argExpr = (Expression) argObj;
|
|
||||||
t.sourceStates.add(argExpr.toString());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "target":
|
|
||||||
// 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();
|
|
||||||
break;
|
|
||||||
case "guard":
|
|
||||||
if (!args.isEmpty()) {
|
|
||||||
Expression guardExpr = (Expression) args.get(0);
|
|
||||||
t.guard = guardExpr.toString();
|
|
||||||
t.isLambdaGuard = isLambdaOrAnonymous(guardExpr);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "action":
|
|
||||||
if (!args.isEmpty()) {
|
|
||||||
Expression actionExpr = (Expression) args.get(0);
|
|
||||||
t.actions.add(actionExpr.toString());
|
|
||||||
t.isLambdaActions.add(isLambdaOrAnonymous(actionExpr));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package com.example.statemachinedemo.app;
|
||||||
|
|
||||||
|
import com.example.statemachinedemo.app.domain.Transition;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class AstTransitionParser {
|
||||||
|
private static final Set<String> SUPPORTED_TRANSITION_TYPES = Set.of(
|
||||||
|
"withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static List<Transition> parseTransitions(MethodDeclaration method) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
Optional.ofNullable(method)
|
||||||
|
.map(MethodDeclaration::getBody)
|
||||||
|
.ifPresent(body -> {
|
||||||
|
for (Object stmt : body.statements()) {
|
||||||
|
if (stmt instanceof ExpressionStatement es
|
||||||
|
&& es.getExpression() instanceof MethodInvocation mi) {
|
||||||
|
transitions.addAll(parseTransitionsFromExpression(mi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
|
||||||
|
// Step 1: Unroll method chain
|
||||||
|
List<MethodInvocation> calls = new ArrayList<>();
|
||||||
|
Expression current = rootCall;
|
||||||
|
while (current instanceof MethodInvocation mi) {
|
||||||
|
calls.addFirst(mi);
|
||||||
|
current = mi.getExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Split by .and()
|
||||||
|
List<List<MethodInvocation>> segments = new ArrayList<>();
|
||||||
|
List<MethodInvocation> currentSegment = new ArrayList<>();
|
||||||
|
for (MethodInvocation call : calls) {
|
||||||
|
if ("and".equals(call.getName().getIdentifier())) {
|
||||||
|
if (!currentSegment.isEmpty()) {
|
||||||
|
segments.add(currentSegment);
|
||||||
|
currentSegment = new ArrayList<>();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentSegment.add(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!currentSegment.isEmpty()) segments.add(currentSegment);
|
||||||
|
|
||||||
|
// Step 3: Parse each segment
|
||||||
|
for (List<MethodInvocation> segment : segments) {
|
||||||
|
boolean isChoice = segment.stream().anyMatch(mi -> "withChoice".equals(mi.getName().getIdentifier()));
|
||||||
|
boolean isJunction = segment.stream().anyMatch(mi -> "withJunction".equals(mi.getName().getIdentifier()));
|
||||||
|
|
||||||
|
if (isChoice || isJunction) {
|
||||||
|
transitions.addAll(parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction"));
|
||||||
|
} else {
|
||||||
|
transitions.add(parseStandardTransition(segment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Transition> parseChoiceOrJunctionTransitions(List<MethodInvocation> segment, String type) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
String sourceState = null;
|
||||||
|
int orderCounter = 0;
|
||||||
|
|
||||||
|
// Extract source state once
|
||||||
|
for (MethodInvocation call : segment) {
|
||||||
|
if ("source".equals(call.getName().getIdentifier()) && !call.arguments().isEmpty()) {
|
||||||
|
sourceState = call.arguments().getFirst().toString();
|
||||||
|
break; // only take the first occurrence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MethodInvocation call : segment) {
|
||||||
|
String methodName = call.getName().getIdentifier();
|
||||||
|
List<?> args = call.arguments();
|
||||||
|
if (!List.of("first", "then", "last").contains(methodName) || args.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Transition t = new Transition();
|
||||||
|
t.setType(type);
|
||||||
|
if (sourceState != null) {
|
||||||
|
t.getSourceStates().add(sourceState);
|
||||||
|
}
|
||||||
|
t.getTargetStates().add(args.get(0).toString());
|
||||||
|
if (args.size() > 1) {
|
||||||
|
parseGuard(args.get(1), t);
|
||||||
|
}
|
||||||
|
if ("then".equals(methodName) && args.size() > 2) {
|
||||||
|
parseAction(args.get(2), t);
|
||||||
|
}
|
||||||
|
t.setOrder(orderCounter++);
|
||||||
|
transitions.add(t);
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Transition parseStandardTransition(List<MethodInvocation> segment) {
|
||||||
|
Transition t = new Transition();
|
||||||
|
for (MethodInvocation call : segment) {
|
||||||
|
String methodName = call.getName().getIdentifier();
|
||||||
|
List<?> args = call.arguments();
|
||||||
|
|
||||||
|
if (SUPPORTED_TRANSITION_TYPES.contains(methodName)) {
|
||||||
|
t.setType(methodName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (methodName) {
|
||||||
|
case "source" -> args.forEach(arg -> t.getSourceStates().add(arg.toString()));
|
||||||
|
case "target" -> args.forEach(arg -> t.getTargetStates().add(arg.toString()));
|
||||||
|
case "event" -> t.setEvent(args.getFirst().toString());
|
||||||
|
case "guard" -> parseGuard(args.getFirst(), t);
|
||||||
|
case "action" -> parseAction(args.getFirst(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseGuard(Object arg, Transition t) {
|
||||||
|
if (arg instanceof Expression expr) {
|
||||||
|
t.setGuard(expr.toString());
|
||||||
|
t.setLambdaGuard(isLambdaOrAnonymous(expr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseAction(Object arg, Transition t) {
|
||||||
|
if (arg instanceof Expression expr) {
|
||||||
|
t.getActions().add(expr.toString());
|
||||||
|
t.getIsLambdaActions().add(isLambdaOrAnonymous(expr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLambdaOrAnonymous(Expression expr) {
|
||||||
|
return switch (expr) {
|
||||||
|
case null -> false;
|
||||||
|
case LambdaExpression le -> true;
|
||||||
|
case ClassInstanceCreation cic -> cic.getAnonymousClassDeclaration() != null;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,28 @@
|
|||||||
package com.example.statemachinedemo;
|
package com.example.statemachinedemo.app;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class AstStateUtils {
|
public class StateMachineStateConfigurationMethodFinder {
|
||||||
|
|
||||||
|
public static CompilationUnit parse(String source) {
|
||||||
|
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||||
|
parser.setSource(source.toCharArray());
|
||||||
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
|
parser.setResolveBindings(false);
|
||||||
|
return (CompilationUnit) parser.createAST(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
public static class StateConfigurerVisitor extends ASTVisitor {
|
public static class StateConfigurerVisitor extends ASTVisitor {
|
||||||
|
|
||||||
private Set<String> initialStates = new HashSet<>();
|
private final Set<String> initialStates = new HashSet<>();
|
||||||
private Set<String> endStates = new HashSet<>();
|
private final Set<String> endStates = new HashSet<>();
|
||||||
|
|
||||||
public Set<String> getInitialStates() {
|
|
||||||
return initialStates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getEndStates() {
|
|
||||||
return endStates;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(MethodDeclaration node) {
|
public boolean visit(MethodDeclaration node) {
|
||||||
@@ -31,13 +36,9 @@ public class AstStateUtils {
|
|||||||
|
|
||||||
// Traverse statements inside configure method
|
// Traverse statements inside configure method
|
||||||
for (Object stmtObj : body.statements()) {
|
for (Object stmtObj : body.statements()) {
|
||||||
if (!(stmtObj instanceof ExpressionStatement)) continue;
|
if (!(stmtObj instanceof ExpressionStatement exprStmt)) continue;
|
||||||
|
|
||||||
ExpressionStatement exprStmt = (ExpressionStatement) stmtObj;
|
|
||||||
Expression expr = exprStmt.getExpression();
|
Expression expr = exprStmt.getExpression();
|
||||||
|
if (expr instanceof MethodInvocation rootCall) {
|
||||||
if (expr instanceof MethodInvocation) {
|
|
||||||
MethodInvocation rootCall = (MethodInvocation) expr;
|
|
||||||
|
|
||||||
// Check if this chain starts with "withStates"
|
// Check if this chain starts with "withStates"
|
||||||
if (isWithStatesChain(rootCall)) {
|
if (isWithStatesChain(rootCall)) {
|
||||||
@@ -50,26 +51,24 @@ public class AstStateUtils {
|
|||||||
|
|
||||||
private boolean isWithStatesChain(MethodInvocation mi) {
|
private boolean isWithStatesChain(MethodInvocation mi) {
|
||||||
MethodInvocation current = mi;
|
MethodInvocation current = mi;
|
||||||
while (current != null) {
|
while (true) {
|
||||||
|
Expression expr = current.getExpression();
|
||||||
if ("withStates".equals(current.getName().getIdentifier())) {
|
if ("withStates".equals(current.getName().getIdentifier())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Expression expr = current.getExpression();
|
if (expr instanceof MethodInvocation next) {
|
||||||
if (expr instanceof MethodInvocation) {
|
current = next;
|
||||||
current = (MethodInvocation) expr;
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
||||||
Expression expr = mi.getExpression();
|
Expression expr = mi.getExpression();
|
||||||
while (expr instanceof MethodInvocation) {
|
while (expr instanceof MethodInvocation parent) {
|
||||||
MethodInvocation parent = (MethodInvocation) expr;
|
String methodName = parent.getName().getIdentifier();
|
||||||
String name = parent.getName().getIdentifier();
|
if (List.of("fork", "region").contains(methodName)) {
|
||||||
if ("fork".equals(name) || "region".equals(name)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
expr = parent.getExpression();
|
expr = parent.getExpression();
|
||||||
@@ -80,37 +79,28 @@ public class AstStateUtils {
|
|||||||
private void parseMethodChain(MethodInvocation mi) {
|
private void parseMethodChain(MethodInvocation mi) {
|
||||||
List<MethodInvocation> chain = new ArrayList<>();
|
List<MethodInvocation> chain = new ArrayList<>();
|
||||||
Expression current = mi;
|
Expression current = mi;
|
||||||
while (current instanceof MethodInvocation) {
|
while (current instanceof MethodInvocation m) {
|
||||||
MethodInvocation m = (MethodInvocation) current;
|
chain.addFirst(m);
|
||||||
chain.add(0, m);
|
|
||||||
current = m.getExpression();
|
current = m.getExpression();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (MethodInvocation call : chain) {
|
for (MethodInvocation call : chain) {
|
||||||
String methodName = call.getName().getIdentifier();
|
String methodName = call.getName().getIdentifier();
|
||||||
List<?> args = call.arguments();
|
List<?> args = call.arguments();
|
||||||
if (args.isEmpty()) continue;
|
if (args.isEmpty()) {
|
||||||
|
continue;
|
||||||
if ("initial".equals(methodName)) {
|
}
|
||||||
|
Expression arg = (Expression) args.getFirst();
|
||||||
|
switch (methodName) {
|
||||||
|
case "initial" -> {
|
||||||
if (!isInsideRegionOrFork(call)) {
|
if (!isInsideRegionOrFork(call)) {
|
||||||
Expression arg = (Expression) args.get(0);
|
|
||||||
initialStates.add(arg.toString());
|
initialStates.add(arg.toString());
|
||||||
}
|
}
|
||||||
} else if ("end".equals(methodName)) {
|
|
||||||
Expression arg = (Expression) args.get(0);
|
|
||||||
endStates.add(arg.toString());
|
|
||||||
}
|
}
|
||||||
// ignore other calls for extraction
|
case "end" -> endStates.add(arg.toString());
|
||||||
|
// ignore others
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.example.statemachinedemo.app;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StateMachineTransitionConfigurationMethodFinder {
|
||||||
|
private final CompilationUnit cu;
|
||||||
|
private final ConfigureMethodVisitor visitor = new ConfigureMethodVisitor();
|
||||||
|
|
||||||
|
public StateMachineTransitionConfigurationMethodFinder(String source) {
|
||||||
|
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||||
|
parser.setSource(source.toCharArray());
|
||||||
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
|
parser.setResolveBindings(false);
|
||||||
|
|
||||||
|
this.cu = (CompilationUnit) parser.createAST(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodDeclaration findConfigureMethod() {
|
||||||
|
this.cu.accept(visitor);
|
||||||
|
return visitor.getConfigureMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static class ConfigureMethodVisitor extends ASTVisitor {
|
||||||
|
private MethodDeclaration configureMethod = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(TypeDeclaration node) {
|
||||||
|
if (!node.isInterface() && extendsStateMachineConfigurerAdapter(node)) {
|
||||||
|
for (MethodDeclaration method : node.getMethods()) {
|
||||||
|
if (isConfigureMethod(method)) {
|
||||||
|
configureMethod = method;
|
||||||
|
return false; // Found, stop visiting further
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration node) {
|
||||||
|
Type superclass = node.getSuperclassType();
|
||||||
|
if (superclass == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (superclass.isParameterizedType()) {
|
||||||
|
ParameterizedType pt = (ParameterizedType) superclass;
|
||||||
|
return pt.getType().toString().equals("StateMachineConfigurerAdapter");
|
||||||
|
}
|
||||||
|
return superclass.toString().equals("StateMachineConfigurerAdapter");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConfigureMethod(MethodDeclaration method) {
|
||||||
|
if (!method.getName().getIdentifier().equals("configure")) return false;
|
||||||
|
|
||||||
|
if (!Modifier.isPublic(method.getModifiers())) return false;
|
||||||
|
Type returnType = method.getReturnType2();
|
||||||
|
if (returnType == null || !returnType.isPrimitiveType() || !"void".equals(returnType.toString()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<SingleVariableDeclaration> params = method.parameters();
|
||||||
|
if (params.size() != 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!params.getFirst().getType().toString().startsWith("StateMachineTransitionConfigurer"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Type> thrownExceptions = method.thrownExceptionTypes();
|
||||||
|
for (Type excType : thrownExceptions) {
|
||||||
|
if (excType.toString().equals("Exception")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.example.statemachinedemo.app;
|
||||||
|
|
||||||
|
import com.example.statemachinedemo.app.domain.Transition;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class TransitionStateUtils {
|
||||||
|
public static Set<String> findStartStates(Collection<Transition> transitions) {
|
||||||
|
Set<String> allTargetStates = transitions.stream()
|
||||||
|
.flatMap(t -> normalizeStates(t.getTargetStates()))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return transitions.stream()
|
||||||
|
.flatMap(t -> normalizeStates(t.getSourceStates()))
|
||||||
|
.filter(state -> !allTargetStates.contains(state))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> findEndStates(Collection<Transition> transitions) {
|
||||||
|
Set<String> allSourceStates = transitions.stream()
|
||||||
|
.flatMap(t -> normalizeStates(t.getSourceStates()))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return transitions.stream()
|
||||||
|
.flatMap(t -> normalizeStates(t.getTargetStates()))
|
||||||
|
.filter(state -> !allSourceStates.contains(state))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<String> normalizeStates(Collection<String> states) {
|
||||||
|
return states.stream()
|
||||||
|
.map(TransitionStateUtils::stripQuotes)
|
||||||
|
.filter(Objects::nonNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripQuotes(String s) {
|
||||||
|
if (s == null) return null;
|
||||||
|
return s.replaceAll("^\"|\"$", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.statemachinedemo.app.domain;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ToString
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Transition {
|
||||||
|
private String type; // withExternal, withChoice, withInternal, etc.
|
||||||
|
private List<String> sourceStates = new ArrayList<>();
|
||||||
|
private List<String> targetStates = new ArrayList<>();
|
||||||
|
private String event;
|
||||||
|
|
||||||
|
private String guard;
|
||||||
|
private boolean isLambdaGuard = false;
|
||||||
|
|
||||||
|
private List<String> actions = new ArrayList<>();
|
||||||
|
private List<Boolean> isLambdaActions = new ArrayList<>();
|
||||||
|
|
||||||
|
private Integer order = null; // optional order for withChoice or withJunction
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.example.statemachinedemo.common;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class FileUtils {
|
||||||
|
public static List<Path> findFilesWithExtension(Path startDir, String extension) throws IOException {
|
||||||
|
try (Stream<Path> paths = Files.walk(startDir)) {
|
||||||
|
return paths
|
||||||
|
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.example.statemachinedemo.in;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class AstFileFinder {
|
||||||
|
private final CompilationUnit cu;
|
||||||
|
|
||||||
|
public AstFileFinder(String source) {
|
||||||
|
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||||
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
|
parser.setResolveBindings(false);
|
||||||
|
parser.setSource(source.toCharArray());
|
||||||
|
this.cu = (CompilationUnit) parser.createAST(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasClassWithAnnotation(String source, String annotationName) {
|
||||||
|
AtomicBoolean found = new AtomicBoolean(false);
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(TypeDeclaration node) {
|
||||||
|
if (node.isInterface()) return true;
|
||||||
|
for (Object modifier : node.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation &&
|
||||||
|
annotation.getTypeName().getFullyQualifiedName().equals(annotationName)) {
|
||||||
|
found.set(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/main/java/com/example/statemachinedemo/out/Dot.java
Normal file
168
src/main/java/com/example/statemachinedemo/out/Dot.java
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package com.example.statemachinedemo.out;
|
||||||
|
|
||||||
|
import com.example.statemachinedemo.app.domain.Transition;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Dot implements StateMachineExporter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String export(List<Transition> transitions,
|
||||||
|
Set<String> startStates,
|
||||||
|
Set<String> endStates,
|
||||||
|
boolean useLambdaGuards) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("digraph stateMachine {\n");
|
||||||
|
sb.append(" rankdir=LR;\n");
|
||||||
|
sb.append(" node [shape=circle, style=filled, fillcolor=lightgray];\n\n");
|
||||||
|
|
||||||
|
// 1. Fake start node
|
||||||
|
sb.append(" start [shape=point, label=\"\"];\n");
|
||||||
|
|
||||||
|
// 2. Find states with choice transitions
|
||||||
|
Set<String> statesWithChoice = new HashSet<>();
|
||||||
|
for (Transition t : transitions) {
|
||||||
|
if ("withChoice".equals(t.getType())) {
|
||||||
|
for (String source : t.getSourceStates()) {
|
||||||
|
statesWithChoice.add(simplify(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Declare normal states and choice pseudostates
|
||||||
|
Set<String> allStates = new HashSet<>();
|
||||||
|
for (Transition t : transitions) {
|
||||||
|
if (t.getSourceStates() != null) {
|
||||||
|
for (String s : t.getSourceStates()) allStates.add(simplify(s));
|
||||||
|
}
|
||||||
|
if (t.getTargetStates() != null) {
|
||||||
|
for (String tgt : t.getTargetStates()) allStates.add(simplify(tgt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all normal states (except choice pseudostates for now)
|
||||||
|
for (String state : allStates) {
|
||||||
|
if (!statesWithChoice.contains(state + "_choice")) {
|
||||||
|
String shape = endStates.contains(state) ? "doublecircle" : "circle";
|
||||||
|
sb.append(" ").append(state)
|
||||||
|
.append(" [shape=").append(shape).append(", fillcolor=lightgray];\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// Declare choice pseudostates as diamonds
|
||||||
|
for (String state : statesWithChoice) {
|
||||||
|
String choiceNode = state + "_choice";
|
||||||
|
sb.append(" ").append(choiceNode)
|
||||||
|
.append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// 4. Connect start node(s) to start states
|
||||||
|
for (String start : startStates) {
|
||||||
|
sb.append(" start -> ").append(simplify(start)).append(";\n");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// 5. Connect normal states to their choice pseudostates
|
||||||
|
for (String state : statesWithChoice) {
|
||||||
|
String choiceNode = state + "_choice";
|
||||||
|
sb.append(" ").append(state).append(" -> ").append(choiceNode).append(";\n");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// 6. Transitions: reroute withChoice transitions via choice pseudostate
|
||||||
|
for (Transition t : transitions) {
|
||||||
|
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
|
||||||
|
|
||||||
|
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
|
||||||
|
|
||||||
|
List<String> targets;
|
||||||
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
|
if (!allowNoTarget) {
|
||||||
|
// self-loop: targets = sources
|
||||||
|
targets = t.getSourceStates();
|
||||||
|
} else {
|
||||||
|
// withJoin/withFork with no targets: skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targets = t.getTargetStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String rawSource : t.getSourceStates()) {
|
||||||
|
String source = simplify(rawSource);
|
||||||
|
|
||||||
|
for (String rawTarget : targets) {
|
||||||
|
String target = simplify(rawTarget);
|
||||||
|
String edgeSource = source;
|
||||||
|
|
||||||
|
if ("withChoice".equals(t.getType()) && statesWithChoice.contains(source)) {
|
||||||
|
edgeSource = source + "_choice";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare guard text
|
||||||
|
String guardText = "";
|
||||||
|
if (t.getGuard() != null && !t.getGuard().isBlank()) {
|
||||||
|
if (useLambdaGuards && t.isLambdaGuard()) {
|
||||||
|
guardText = "lambda";
|
||||||
|
} else {
|
||||||
|
guardText = t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare actions text
|
||||||
|
String actionsText = "";
|
||||||
|
if (t.getActions() != null && !t.getActions().isEmpty()) {
|
||||||
|
StringBuilder actionBuilder = new StringBuilder();
|
||||||
|
for (int i = 0; i < t.getActions().size(); i++) {
|
||||||
|
if (i > 0) actionBuilder.append(", ");
|
||||||
|
String action = t.getActions().get(i);
|
||||||
|
boolean isLambda = t.getIsLambdaActions().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.getEvent() != null && !t.getEvent().isBlank()) {
|
||||||
|
label.append(simplify(t.getEvent()));
|
||||||
|
}
|
||||||
|
if (!guardText.isBlank()) {
|
||||||
|
if (!label.isEmpty()) label.append(" ");
|
||||||
|
label.append("[").append(guardText).append("]");
|
||||||
|
}
|
||||||
|
if (!actionsText.isBlank()) {
|
||||||
|
if (!label.isEmpty()) label.append(" / ");
|
||||||
|
label.append(actionsText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add order info for choice/junction transitions
|
||||||
|
if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) {
|
||||||
|
if (!label.isEmpty()) label.append(" ");
|
||||||
|
label.append("(order=").append(t.getOrder()).append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
String edgeLabel = !label.isEmpty() ? " [label=\"" + label.toString() + "\"]" : "";
|
||||||
|
|
||||||
|
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("}\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFileExtension() {
|
||||||
|
return ".dot";
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/main/java/com/example/statemachinedemo/out/PlantUml.java
Normal file
184
src/main/java/com/example/statemachinedemo/out/PlantUml.java
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package com.example.statemachinedemo.out;
|
||||||
|
|
||||||
|
import com.example.statemachinedemo.app.domain.Transition;
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class PlantUml implements StateMachineExporter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String export(List<Transition> transitions,
|
||||||
|
Set<String> startStates,
|
||||||
|
Set<String> endStates,
|
||||||
|
boolean useLambdaGuards) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("@startuml\n");
|
||||||
|
sb.append("skinparam state {\n");
|
||||||
|
sb.append(" BackgroundColor<<choice>> LightYellow\n");
|
||||||
|
sb.append("}\n\n");
|
||||||
|
|
||||||
|
// 1. Print start states
|
||||||
|
for (String start : startStates) {
|
||||||
|
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// 2. Find all states that have choice transitions originating from them
|
||||||
|
Set<String> statesWithChoice = new HashSet<>();
|
||||||
|
for (Transition t : transitions) {
|
||||||
|
if ("withChoice".equals(t.getType())) {
|
||||||
|
for (String source : t.getSourceStates()) {
|
||||||
|
statesWithChoice.add(simplify(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Declare choice pseudostates & connect from original state
|
||||||
|
for (String state : statesWithChoice) {
|
||||||
|
String choiceState = state + "_choice";
|
||||||
|
sb.append("state ").append(choiceState).append(" <<choice>>\n");
|
||||||
|
sb.append(state).append(" --> ").append(choiceState).append("\n");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// Color palette for choices
|
||||||
|
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
||||||
|
|
||||||
|
// Assign color to each choice state
|
||||||
|
Map<String, String> choiceStateColorMap = new HashMap<>();
|
||||||
|
int colorIndex = 0;
|
||||||
|
for (String state : statesWithChoice) {
|
||||||
|
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
|
||||||
|
colorIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> transitionLines = new HashSet<>();
|
||||||
|
|
||||||
|
// 4. Output transitions
|
||||||
|
for (Transition t : transitions) {
|
||||||
|
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
|
||||||
|
|
||||||
|
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
|
||||||
|
|
||||||
|
List<String> targets;
|
||||||
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
|
if (!allowNoTarget) {
|
||||||
|
targets = t.getSourceStates(); // Self-loop
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targets = t.getTargetStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String rawSource : t.getSourceStates()) {
|
||||||
|
String source = simplify(rawSource);
|
||||||
|
|
||||||
|
for (String rawTarget : targets) {
|
||||||
|
String target = simplify(rawTarget);
|
||||||
|
String transitionSource = "withChoice".equals(t.getType()) && statesWithChoice.contains(source)
|
||||||
|
? source + "_choice"
|
||||||
|
: source;
|
||||||
|
|
||||||
|
String label = buildLabel(t, useLambdaGuards);
|
||||||
|
String color = getColor(t, source, choiceStateColorMap);
|
||||||
|
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
|
||||||
|
|
||||||
|
if (transitionLines.add(line)) {
|
||||||
|
sb.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("\n");
|
||||||
|
|
||||||
|
// 5. Mark end states
|
||||||
|
for (String end : endStates) {
|
||||||
|
sb.append(simplify(end)).append(" --> [*]\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("@enduml\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFileExtension() {
|
||||||
|
return ".plantuml.dot";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getGuardText(Transition t, boolean useLambdaGuards) {
|
||||||
|
String guard = t.getGuard();
|
||||||
|
if (StringUtils.isBlank(guard)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (useLambdaGuards && t.isLambdaGuard()) {
|
||||||
|
return "lambda";
|
||||||
|
}
|
||||||
|
return guard
|
||||||
|
.replaceAll("[\\n\\r]+", " ")
|
||||||
|
.replaceAll("\\s{2,}", " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getActionsText(Transition t, boolean useLambdaGuards) {
|
||||||
|
if (t.getActions() == null || t.getActions().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> actions = t.getActions();
|
||||||
|
List<Boolean> isLambdaFlags = t.getIsLambdaActions();
|
||||||
|
|
||||||
|
// Defensive: ensure lists are same size to avoid IndexOutOfBoundsException
|
||||||
|
int size = Math.min(actions.size(), isLambdaFlags.size());
|
||||||
|
|
||||||
|
return IntStream.range(0, size)
|
||||||
|
.mapToObj(i -> (useLambdaGuards && isLambdaFlags.get(i)) ? "lambda" : actions.get(i))
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildLabel(Transition t, boolean useLambdaGuards) {
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
Optional.ofNullable(t.getEvent())
|
||||||
|
.filter(e -> !e.isBlank())
|
||||||
|
.map(this::simplify)
|
||||||
|
.ifPresent(parts::add);
|
||||||
|
Optional.of(getGuardText(t, useLambdaGuards))
|
||||||
|
.filter(g -> !g.isBlank())
|
||||||
|
.map(g -> "[" + g + "]")
|
||||||
|
.ifPresent(parts::add);
|
||||||
|
Optional.of(getActionsText(t, useLambdaGuards))
|
||||||
|
.filter(a -> !a.isBlank())
|
||||||
|
.ifPresent(actions -> {
|
||||||
|
if (!parts.isEmpty()) {
|
||||||
|
int lastIndex = parts.size() - 1;
|
||||||
|
String last = parts.get(lastIndex);
|
||||||
|
parts.set(lastIndex, last + " / " + actions);
|
||||||
|
} else {
|
||||||
|
parts.add(actions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Optional.ofNullable(t.getOrder())
|
||||||
|
.filter(order -> List.of("withChoice", "withJunction").contains(t.getType()))
|
||||||
|
.map(order -> "(order=" + order + ")")
|
||||||
|
.ifPresent(parts::add);
|
||||||
|
|
||||||
|
return String.join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getColor(Transition t, String source, Map<String, String> choiceStateColorMap) {
|
||||||
|
return switch (t.getType()) {
|
||||||
|
case "withChoice" -> choiceStateColorMap.getOrDefault(source, "000000");
|
||||||
|
case "withExternal" -> "1E90FF";
|
||||||
|
case "withInternal" -> "32CD32";
|
||||||
|
case "withLocal" -> "FFA500";
|
||||||
|
case "withJunction" -> "FF69B4";
|
||||||
|
case "withJoin" -> "8A2BE2";
|
||||||
|
case "withFork" -> "20B2AA";
|
||||||
|
default -> "000000";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/main/java/com/example/statemachinedemo/out/Scxml.java
Normal file
112
src/main/java/com/example/statemachinedemo/out/Scxml.java
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package com.example.statemachinedemo.out;
|
||||||
|
|
||||||
|
import com.example.statemachinedemo.app.domain.Transition;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Scxml implements StateMachineExporter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String export(List<Transition> transitions,
|
||||||
|
Set<String> startStates,
|
||||||
|
Set<String> endStates,
|
||||||
|
boolean useLambdaGuards) {
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||||
|
sb.append("<scxml xmlns=\"http://www.w3.org/2005/07/scxml\" version=\"1.0\">\n\n");
|
||||||
|
|
||||||
|
// 1. Collect all states
|
||||||
|
Set<String> allStates = new HashSet<>();
|
||||||
|
for (Transition t : transitions) {
|
||||||
|
if (t.getSourceStates() != null) allStates.addAll(t.getSourceStates());
|
||||||
|
if (t.getTargetStates() != null) allStates.addAll(t.getTargetStates());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Emit <state> elements
|
||||||
|
for (String state : allStates) {
|
||||||
|
String simpleState = simplify(state);
|
||||||
|
sb.append(" <state id=\"").append(simpleState).append("\">\n");
|
||||||
|
|
||||||
|
if (startStates.contains(state)) {
|
||||||
|
sb.append(" <initial>\n");
|
||||||
|
sb.append(" <transition target=\"").append(simpleState).append("\"/>\n");
|
||||||
|
sb.append(" </initial>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Transition t : transitions) {
|
||||||
|
if (t.getSourceStates() == null || !t.getSourceStates().contains(state)) continue;
|
||||||
|
|
||||||
|
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
|
||||||
|
List<String> targets;
|
||||||
|
|
||||||
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
|
if (!allowNoTarget) {
|
||||||
|
targets = List.of(state); // self-loop
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targets = t.getTargetStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String rawTarget : targets) {
|
||||||
|
String target = simplify(rawTarget);
|
||||||
|
if (target.isEmpty()) continue;
|
||||||
|
|
||||||
|
sb.append(renderTransition(t, target, useLambdaGuards));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(" </state>\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("</scxml>\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFileExtension() {
|
||||||
|
return ".scxml.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getGuardText(Transition t, boolean useLambdaGuards) {
|
||||||
|
if (t.getGuard() == null || t.getGuard().isBlank()) return "";
|
||||||
|
if (useLambdaGuards && t.isLambdaGuard()) return "lambda";
|
||||||
|
return t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeXml(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
.replace("\"", """).replace("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderTransition(Transition t, String target, boolean useLambdaGuards) {
|
||||||
|
StringBuilder sb = new StringBuilder(" <transition");
|
||||||
|
|
||||||
|
if (t.getEvent() != null && !t.getEvent().isBlank()) {
|
||||||
|
sb.append(" event=\"").append(simplify(t.getEvent())).append("\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(" target=\"").append(target).append("\"");
|
||||||
|
|
||||||
|
String guard = getGuardText(t, useLambdaGuards);
|
||||||
|
if (!guard.isBlank()) {
|
||||||
|
sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choice/junction order → XML comment
|
||||||
|
if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) {
|
||||||
|
sb.append(">\n");
|
||||||
|
sb.append(" <!-- order=").append(t.getOrder()).append(" -->\n");
|
||||||
|
sb.append(" </transition>\n");
|
||||||
|
} else {
|
||||||
|
sb.append("/>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.example.statemachinedemo.out;
|
||||||
|
|
||||||
|
import com.example.statemachinedemo.app.domain.Transition;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public interface StateMachineExporter {
|
||||||
|
String export(List<Transition> transitions,
|
||||||
|
Set<String> startStates,
|
||||||
|
Set<String> endStates,
|
||||||
|
boolean useLambdaGuards);
|
||||||
|
|
||||||
|
// Helper: extract last enum part
|
||||||
|
default String simplify(String full) {
|
||||||
|
if (full == null) return "";
|
||||||
|
int dot = full.lastIndexOf('.');
|
||||||
|
return dot >= 0 ? full.substring(dot + 1) : full;
|
||||||
|
}
|
||||||
|
String getFileExtension(); // e.g. ".dot", ".plantuml.dot", ".scxml.xml"
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.example.statemachinedemo.notgoodspringbased;
|
package com.example.statemachinedemo.z.notgoodspringbased;
|
||||||
|
|
||||||
import com.example.statemachinedemo.statemachine.OrderEvents;
|
import com.example.statemachinedemo.z.statemachine.OrderEvents;
|
||||||
import com.example.statemachinedemo.statemachine.OrderStates;
|
import com.example.statemachinedemo.z.statemachine.OrderStates;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
import org.springframework.statemachine.StateMachine;
|
import org.springframework.statemachine.StateMachine;
|
||||||
@@ -34,8 +35,7 @@ public class SpringExporter {
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
class Runner implements ApplicationRunner {
|
class Runner implements ApplicationRunner {
|
||||||
private final StateMachineFactory<OrderStates, OrderEvents> factory;
|
// private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
|
||||||
//
|
|
||||||
// @Override
|
// @Override
|
||||||
// public void run(ApplicationArguments args) throws Exception {
|
// public void run(ApplicationArguments args) throws Exception {
|
||||||
// StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
|
// StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
|
||||||
@@ -51,7 +51,7 @@ class Runner implements ApplicationRunner {
|
|||||||
public void run(ApplicationArguments args) throws Exception {
|
public void run(ApplicationArguments args) throws Exception {
|
||||||
c.a();
|
c.a();
|
||||||
c2.a();
|
c2.a();
|
||||||
// c3.exportToSCXML();
|
c3.exportToSCXML();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +126,13 @@ class StateMachinePlantUMLExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
class Config1 {
|
class Config1 {
|
||||||
private final StateMachineFactory<OrderStates, OrderEvents> factory;
|
@Qualifier("simpleEnumStateMachineFactory")
|
||||||
|
private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
|
||||||
public void a() throws FileNotFoundException {
|
public void a() throws FileNotFoundException {
|
||||||
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
|
StateMachine<OrderStates, OrderEvents> stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order");
|
||||||
|
|
||||||
StateMachineDotExporter exporter = new StateMachineDotExporter();
|
StateMachineDotExporter exporter = new StateMachineDotExporter();
|
||||||
try (PrintWriter out = new PrintWriter("statemachine.dot")) {
|
try (PrintWriter out = new PrintWriter("statemachine.dot")) {
|
||||||
@@ -144,9 +144,10 @@ class Config1 {
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
class Config2 {
|
class Config2 {
|
||||||
private final StateMachineFactory<OrderStates, OrderEvents> factory;
|
@Qualifier("simpleEnumStateMachineFactory")
|
||||||
|
private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
|
||||||
public void a() throws FileNotFoundException {
|
public void a() throws FileNotFoundException {
|
||||||
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
|
StateMachine<OrderStates, OrderEvents> stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order");
|
||||||
|
|
||||||
try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) {
|
try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) {
|
||||||
StateMachinePlantUMLExporter.export(stateMachine, out);
|
StateMachinePlantUMLExporter.export(stateMachine, out);
|
||||||
@@ -156,12 +157,13 @@ class Config2 {
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
class Config3 {
|
class Config3 {
|
||||||
private final StateMachineFactory<OrderStates, OrderEvents> factory;
|
@Qualifier("simpleEnumStateMachineFactory")
|
||||||
|
private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
|
||||||
public void exportToSCXML() throws Exception {
|
public void exportToSCXML() throws Exception {
|
||||||
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
|
StateMachine<OrderStates, OrderEvents> stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order");
|
||||||
|
|
||||||
SCXMLExporter exporter = new SCXMLExporter();
|
SCXMLExporter exporter = new SCXMLExporter();
|
||||||
File file = new File("statemachine.scxml");
|
File file = new File("statemachine.scxml.xml");
|
||||||
exporter.exportToSCXML(stateMachine);
|
exporter.exportToSCXML(stateMachine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
package com.example.statemachinedemo.statemachine;
|
package com.example.statemachinedemo.z.statemachine;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.statemachine.StateContext;
|
|
||||||
import org.springframework.statemachine.action.Action;
|
import org.springframework.statemachine.action.Action;
|
||||||
import org.springframework.statemachine.config.EnableStateMachine;
|
|
||||||
import org.springframework.statemachine.config.EnableStateMachineFactory;
|
import org.springframework.statemachine.config.EnableStateMachineFactory;
|
||||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
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.StateMachineConfigurationConfigurer;
|
||||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||||
@@ -15,7 +11,7 @@ import org.springframework.statemachine.guard.Guard;
|
|||||||
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableStateMachineFactory
|
@EnableStateMachineFactory(name = "complexStateMachineFactory")
|
||||||
public class ComplexStateMachineConfig extends StateMachineConfigurerAdapter<ComplexStateMachineConfig.States, ComplexStateMachineConfig.Events> {
|
public class ComplexStateMachineConfig extends StateMachineConfigurerAdapter<ComplexStateMachineConfig.States, ComplexStateMachineConfig.Events> {
|
||||||
|
|
||||||
public enum States {
|
public enum States {
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
package com.example.statemachinedemo.statemachine;
|
package com.example.statemachinedemo.z.statemachine;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.statemachine.action.Action;
|
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.StateMachineConfigurerAdapter;
|
||||||
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
|
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
|
||||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||||
import org.springframework.statemachine.state.EnumState;
|
|
||||||
import org.springframework.statemachine.guard.Guard;
|
import org.springframework.statemachine.guard.Guard;
|
||||||
|
|
||||||
import java.util.EnumSet;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableStateMachine
|
@EnableStateMachineFactory(name = "forkJoinStateMachineFactory")
|
||||||
public class ForkJoinStateMachineConfig extends StateMachineConfigurerAdapter<ForkJoinStateMachineConfig.States, ForkJoinStateMachineConfig.Events> {
|
public class ForkJoinStateMachineConfig extends StateMachineConfigurerAdapter<ForkJoinStateMachineConfig.States, ForkJoinStateMachineConfig.Events> {
|
||||||
|
|
||||||
public enum States {
|
public enum States {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.statemachinedemo.statemachine;
|
package com.example.statemachinedemo.z.statemachine;
|
||||||
|
|
||||||
public enum OrderEvents {
|
public enum OrderEvents {
|
||||||
FULFILL, PAY, CANCEL,
|
FULFILL, PAY, CANCEL,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.statemachinedemo.statemachine;
|
package com.example.statemachinedemo.z.statemachine;
|
||||||
|
|
||||||
public enum OrderStates {
|
public enum OrderStates {
|
||||||
SUBMITTED, PAID, FULFILLED, CANCELED
|
SUBMITTED, PAID, FULFILLED, CANCELED
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.statemachinedemo.statemachine;
|
package com.example.statemachinedemo.z.statemachine;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -14,7 +14,7 @@ import org.springframework.statemachine.listener.StateMachineListenerAdapter;
|
|||||||
import org.springframework.statemachine.state.State;
|
import org.springframework.statemachine.state.State;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableStateMachineFactory
|
@EnableStateMachineFactory(name = "simpleEnumStateMachineFactory")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||||
@Override
|
@Override
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
digraph stateMachine {
|
|
||||||
rankdir=LR;
|
|
||||||
node [shape=circle, style=filled, fillcolor=lightgray];
|
|
||||||
|
|
||||||
start [shape=point, label=""];
|
|
||||||
FULFILLED [shape=circle, fillcolor=lightgray];
|
|
||||||
PAID1 [shape=circle, fillcolor=lightgray];
|
|
||||||
PAID3 [shape=circle, fillcolor=lightgray];
|
|
||||||
PAID2 [shape=circle, fillcolor=lightgray];
|
|
||||||
HAPPEN [shape=circle, fillcolor=lightgray];
|
|
||||||
CANCELED [shape=circle, fillcolor=lightgray];
|
|
||||||
PAID [shape=circle, fillcolor=lightgray];
|
|
||||||
SUBMITTED [shape=circle, fillcolor=lightgray];
|
|
||||||
|
|
||||||
PAID_choice [shape=diamond, label="", fillcolor=lightyellow];
|
|
||||||
SUBMITTED_choice [shape=diamond, label="", fillcolor=lightyellow];
|
|
||||||
|
|
||||||
start -> SUBMITTED;
|
|
||||||
|
|
||||||
PAID -> PAID_choice;
|
|
||||||
SUBMITTED -> SUBMITTED_choice;
|
|
||||||
|
|
||||||
SUBMITTED -> PAID [label="PAY"];
|
|
||||||
PAID -> FULFILLED [label="FULFILL"];
|
|
||||||
SUBMITTED -> CANCELED [label="CANCEL [lambda]"];
|
|
||||||
PAID -> CANCELED [label="CANCEL"];
|
|
||||||
SUBMITTED -> SUBMITTED [label="ABCD"];
|
|
||||||
SUBMITTED_choice -> PAID2 [label="[lambda] (order=0)"];
|
|
||||||
SUBMITTED_choice -> PAID3 [label="(order=1)"];
|
|
||||||
PAID_choice -> PAID1 [label="[lambda] (order=0)"];
|
|
||||||
PAID_choice -> PAID2 [label="[guard1] (order=1)"];
|
|
||||||
PAID_choice -> HAPPEN [label="[lambda] (order=2)"];
|
|
||||||
PAID_choice -> PAID3 [label="(order=3)"];
|
|
||||||
PAID2 -> CANCELED;
|
|
||||||
PAID3 -> CANCELED;
|
|
||||||
PAID1 -> PAID1 [label="ABCD / lambda, action2"];
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
@startuml
|
|
||||||
skinparam state {
|
|
||||||
BackgroundColor<<choice>> LightYellow
|
|
||||||
}
|
|
||||||
|
|
||||||
[*] --> SUBMITTED
|
|
||||||
|
|
||||||
state PAID_choice <<choice>>
|
|
||||||
PAID --> PAID_choice
|
|
||||||
state SUBMITTED_choice <<choice>>
|
|
||||||
SUBMITTED --> SUBMITTED_choice
|
|
||||||
|
|
||||||
SUBMITTED -[#1E90FF,bold]-> PAID : PAY
|
|
||||||
PAID -[#1E90FF,bold]-> FULFILLED : FULFILL
|
|
||||||
SUBMITTED -[#1E90FF,bold]-> CANCELED : CANCEL [lambda]
|
|
||||||
PAID -[#1E90FF,bold]-> CANCELED : CANCEL
|
|
||||||
SUBMITTED -[#1E90FF,bold]-> SUBMITTED : ABCD
|
|
||||||
SUBMITTED_choice -[#4682B4,bold]-> PAID2 : [lambda] (order=0)
|
|
||||||
SUBMITTED_choice -[#4682B4,bold]-> PAID3 : (order=1)
|
|
||||||
PAID_choice -[#FF6347,bold]-> PAID1 : [lambda] (order=0)
|
|
||||||
PAID_choice -[#FF6347,bold]-> PAID2 : [guard1] (order=1)
|
|
||||||
PAID_choice -[#FF6347,bold]-> HAPPEN : [lambda] (order=2)
|
|
||||||
PAID_choice -[#FF6347,bold]-> PAID3 : (order=3)
|
|
||||||
PAID2 -[#1E90FF,bold]-> CANCELED
|
|
||||||
PAID3 -[#1E90FF,bold]-> CANCELED
|
|
||||||
PAID1 -[#1E90FF,bold]-> PAID1 : ABCD / lambda, action2
|
|
||||||
|
|
||||||
PAID1 --> [*]
|
|
||||||
CANCELED --> [*]
|
|
||||||
FULFILLED --> [*]
|
|
||||||
HAPPEN --> [*]
|
|
||||||
@enduml
|
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
@@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0">
|
|
||||||
|
|
||||||
<state id="PAID1">
|
|
||||||
<transition event="ABCD" target="PAID1"/>
|
|
||||||
</state>
|
|
||||||
|
|
||||||
<state id="SUBMITTED">
|
|
||||||
<initial>
|
|
||||||
<transition target="SUBMITTED"/>
|
|
||||||
</initial>
|
|
||||||
<transition event="PAY" target="PAID"/>
|
|
||||||
<transition event="CANCEL" target="CANCELED" cond="lambda"/>
|
|
||||||
<transition event="ABCD" target="SUBMITTED"/>
|
|
||||||
<transition target="PAID2" cond="lambda">
|
|
||||||
<!-- order=0 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="PAID3">
|
|
||||||
<!-- order=1 -->
|
|
||||||
</transition>
|
|
||||||
</state>
|
|
||||||
|
|
||||||
<state id="PAID">
|
|
||||||
<transition event="FULFILL" target="FULFILLED"/>
|
|
||||||
<transition event="CANCEL" target="CANCELED"/>
|
|
||||||
<transition target="PAID1" cond="lambda">
|
|
||||||
<!-- order=0 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="PAID2" cond="guard1">
|
|
||||||
<!-- order=1 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="HAPPEN" cond="lambda">
|
|
||||||
<!-- order=2 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="PAID3">
|
|
||||||
<!-- order=3 -->
|
|
||||||
</transition>
|
|
||||||
</state>
|
|
||||||
|
|
||||||
<state id="CANCELED">
|
|
||||||
</state>
|
|
||||||
|
|
||||||
<state id="FULFILLED">
|
|
||||||
</state>
|
|
||||||
|
|
||||||
<state id="PAID3">
|
|
||||||
<transition target="CANCELED"/>
|
|
||||||
</state>
|
|
||||||
|
|
||||||
<state id="HAPPEN">
|
|
||||||
</state>
|
|
||||||
|
|
||||||
<state id="PAID2">
|
|
||||||
<transition target="CANCELED"/>
|
|
||||||
</state>
|
|
||||||
|
|
||||||
</scxml>
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
digraph stateMachine {
|
|
||||||
SUBMITTED;
|
|
||||||
CANCELED;
|
|
||||||
PAID;
|
|
||||||
FULFILLED;
|
|
||||||
PAID2;
|
|
||||||
PAID3;
|
|
||||||
SUBMITTED -> PAID [label="PAY"];
|
|
||||||
PAID -> FULFILLED [label="FULFILL"];
|
|
||||||
SUBMITTED -> CANCELED [label="CANCEL"];
|
|
||||||
PAID -> CANCELED [label="CANCEL"];
|
|
||||||
PAID2 -> CANCELED [label=""];
|
|
||||||
PAID3 -> CANCELED [label=""];
|
|
||||||
}
|
|
||||||
BIN
statemachine.png
BIN
statemachine.png
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
@@ -1,11 +0,0 @@
|
|||||||
@startuml
|
|
||||||
[*] --> SUBMITTED
|
|
||||||
SUBMITTED --> PAID : PAY
|
|
||||||
PAID --> FULFILLED : FULFILL
|
|
||||||
SUBMITTED --> CANCELED : CANCEL [guard]
|
|
||||||
PAID --> CANCELED : CANCEL
|
|
||||||
PAID2 --> CANCELED
|
|
||||||
PAID3 --> CANCELED
|
|
||||||
CANCELED --> [*]
|
|
||||||
FULFILLED --> [*]
|
|
||||||
@enduml
|
|
||||||
Reference in New Issue
Block a user