This commit is contained in:
2025-07-13 12:18:23 +02:00
parent c59947a0ac
commit 5de15f8a68
16 changed files with 542 additions and 492 deletions

View File

@@ -1,394 +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;
import static com.example.statemachinedemo.ExportUtils.*;
public class AFinder {
private static final String START_DIR = ".";
private static final String EXTENSION = ".java";
private static final String TARGET_ANNOTATION = "EnableStateMachineFactory";
private static final String TARGET_SUPERCLASS = "StateMachineConfigurerAdapter";
private static final String TARGET_METHOD_NAME = "configure";
private static final String TARGET_METHOD_PARAM = "StateMachineTransitionConfigurer";
private static final String TARGET_THROWS_EXCEPTION = "Exception";
public static void main(String[] args) throws IOException {
List<Path> javaFiles = findJavaFiles(Paths.get(START_DIR));
for (Path javaFile : javaFiles) {
String content = Files.readString(javaFile);
boolean findAnnotations = false;
if (findAnnotations) {
if (hasClassWithAnnotation(content, TARGET_ANNOTATION)) {
System.out.println("Found annotation in: " + javaFile.toAbsolutePath());
}
}
boolean printBody = false;
if (printBody) {
if (hasClassWithSuperAndConfigureMethodExactSignature(
content,
TARGET_SUPERCLASS,
TARGET_METHOD_NAME,
TARGET_METHOD_PARAM,
TARGET_THROWS_EXCEPTION)) {
System.out.println("Found class with exact configure signature in: " + javaFile.toAbsolutePath());
}
String methodBody = getConfigureMethodBodyExactSignature(
content,
TARGET_SUPERCLASS,
TARGET_METHOD_NAME,
TARGET_METHOD_PARAM,
TARGET_THROWS_EXCEPTION);
if (methodBody != null) {
System.out.println("Method body:\n" + methodBody);
}
}
String source = Files.readString(javaFile.toAbsolutePath());
MethodDeclaration configureMethod = ConfigureMethodFinder.findConfigureMethod(source);
if (configureMethod != null) {
System.out.println("Found configure method!");
// Now pass configureMethod to your TransitionParser.parseTransitions(configureMethod)
List<Transition> transitions = TransitionParser.parseTransitions(configureMethod);
for (Transition t : transitions) {
System.out.println(t);
}
// Find start and end states
Set<String> startStates = StateUtils.findStartStates(transitions);
Set<String> endStates = StateUtils.findEndStates(transitions);
System.out.println("Start States: " + startStates);
System.out.println("End States: " + endStates);
String s = generatePlantUML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.2.dot")) {
out.println(s);
}
String s1 = generateDot(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.1.dot")) {
out.println(s1);
}
String s2 = exportToSCXML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.3.xml")) {
out.println(s2);
}
}
// else {
// System.out.println("No configure method matching criteria found.");
// }
}
}
private static List<Path> findJavaFiles(Path startDir) throws IOException {
try (Stream<Path> paths = Files.walk(startDir)) {
return paths
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(EXTENSION))
.collect(Collectors.toList());
}
}
private 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];
}
private static String getConfigureMethodBodyExactSignature(
String source,
String targetSuperclassName,
String methodName,
String methodParamType,
String throwsExceptionType) {
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 String[] bodyString = {null};
cu.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
if (node.isInterface()) return true;
Type superType = node.getSuperclassType();
if (superType != null) {
String superName = superType.toString();
if (superName.startsWith(targetSuperclassName)) {
MethodDeclaration[] methods = node.getMethods();
for (MethodDeclaration method : methods) {
if (!method.getName().getIdentifier().equals(methodName)) {
continue;
}
Type returnType = method.getReturnType2();
if (returnType == null || !returnType.isPrimitiveType() ||
((PrimitiveType) returnType).getPrimitiveTypeCode() != PrimitiveType.VOID) {
continue;
}
boolean hasOverride = false;
for (Object mod : method.modifiers()) {
if (mod instanceof Annotation) {
Annotation annotation = (Annotation) mod;
if (annotation.getTypeName().getFullyQualifiedName().equals("Override")) {
hasOverride = true;
break;
}
}
}
if (!hasOverride) continue;
List<?> parameters = method.parameters();
if (parameters.size() != 1) continue;
SingleVariableDeclaration param = (SingleVariableDeclaration) parameters.get(0);
String paramTypeName = param.getType().toString();
if (!paramTypeName.startsWith(methodParamType)) continue;
@SuppressWarnings("unchecked")
List<Type> thrownExceptions = method.thrownExceptionTypes();
boolean throwsExceptionFound = false;
for (Type excType : thrownExceptions) {
if (excType.toString().equals(throwsExceptionType)) {
throwsExceptionFound = true;
break;
}
}
if (!throwsExceptionFound) continue;
Block body = method.getBody();
if (body != null) {
bodyString[0] = body.toString();
}
return false;
}
}
}
return true;
}
});
return bodyString[0];
}
private static boolean hasClassWithSuperAndConfigureMethodExactSignature(
String source,
String targetSuperclassName,
String methodName,
String methodParamType,
String throwsExceptionType) {
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;
Type superType = node.getSuperclassType();
if (superType != null) {
String superName = superType.toString();
if (superName.startsWith(targetSuperclassName)) {
MethodDeclaration[] methods = node.getMethods();
for (MethodDeclaration method : methods) {
// 1) Check method name
if (!method.getName().getIdentifier().equals(methodName)) {
continue;
}
// 2) Check return type is void
Type returnType = method.getReturnType2();
if (returnType == null || !returnType.isPrimitiveType() ||
((PrimitiveType) returnType).getPrimitiveTypeCode() != PrimitiveType.VOID) {
continue;
}
// 3) Check @Override annotation present
boolean hasOverride = false;
for (Object mod : method.modifiers()) {
if (mod instanceof Annotation) {
Annotation annotation = (Annotation) mod;
if (annotation.getTypeName().getFullyQualifiedName().equals("Override")) {
hasOverride = true;
break;
}
}
}
if (!hasOverride) continue;
// 4) Check parameters - exactly 1 parameter of the right type
List<?> parameters = method.parameters();
if (parameters.size() != 1) continue;
SingleVariableDeclaration param = (SingleVariableDeclaration) parameters.get(0);
String paramTypeName = param.getType().toString();
if (!paramTypeName.startsWith(methodParamType)) continue;
// 5) Check throws clause includes Exception
@SuppressWarnings("unchecked")
List<Type> thrownExceptions = method.thrownExceptionTypes();
boolean throwsExceptionFound = false;
for (Type excType : thrownExceptions) {
String excName = excType.toString();
if (excName.equals(throwsExceptionType)) {
throwsExceptionFound = true;
break;
}
}
if (!throwsExceptionFound) continue;
// All conditions met!
found[0] = true;
return false;
}
}
}
return true;
}
});
return found[0];
}
}
class ConfigureMethodFinder {
/**
* 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;
}
}
}

View File

@@ -0,0 +1,50 @@
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];
}
}

View File

@@ -1,15 +1,12 @@
package com.example.statemachinedemo;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
public class ExportUtils {
public static String generatePlantUML(List<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards) {
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards) {
StringBuilder sb = new StringBuilder();
sb.append("@startuml\n");
sb.append("skinparam state {\n");
@@ -23,7 +20,6 @@ public class ExportUtils {
sb.append("\n");
// 2. Find all states that have choice transitions originating from them
// We'll reroute those transitions through a separate choice pseudostate.
Set<String> statesWithChoice = new HashSet<>();
for (Transition t : transitions) {
if ("withChoice".equals(t.type)) {
@@ -39,9 +35,28 @@ public class ExportUtils {
}
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<>();
// 4. Output transitions:
// Helper to get color for non-choice transitions (NO leading '#')
java.util.function.Function<String, String> getTransitionColor = (type) -> {
if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue
if ("withInternal".equals(type)) return "32CD32"; // LimeGreen
if ("withJunction".equals(type)) return "FF69B4"; // HotPink
return "000000"; // Black fallback
};
// 4. Output transitions
for (Transition t : transitions) {
if (t.sourceState == null || t.targetState == null) continue;
@@ -58,20 +73,60 @@ public class ExportUtils {
// Prepare guard text
String guardText = "";
if (t.guard != null && !t.guard.isBlank()) {
guardText = useLambdaGuards ? "lambda"
: t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
if (useLambdaGuards && t.isLambdaGuard) {
guardText = "lambda";
} else {
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
}
}
// Build label with event and guard
// 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()) {
label.append(" [").append(guardText).append("]");
if (label.length() > 0) label.append(" ");
label.append("[").append(guardText).append("]");
}
if (!actionsText.isBlank()) {
if (label.length() > 0) label.append(" / ");
label.append(actionsText);
}
String line = transitionSource + " --> " + target + (label.isEmpty() ? "" : (" : " + label));
// 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)) {
@@ -89,6 +144,7 @@ public class ExportUtils {
sb.append("@enduml\n");
return sb.toString();
}
public static String generateDot(List<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
@@ -159,18 +215,51 @@ public class ExportUtils {
edgeSource = source + "_choice";
}
// Prepare guard text
String guardText = "";
if (t.guard != null && !t.guard.isBlank()) {
guardText = useLambdaGuards ? "lambda"
: t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
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()) {
label.append(" [").append(guardText).append("]");
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() + "\"]" : "";
@@ -216,42 +305,38 @@ public class ExportUtils {
if (target == null || target.isEmpty()) continue;
String event = t.event != null ? simplify(t.event) : null;
String guard = t.guard != null && !t.guard.isBlank()
? (useLambdaGuards ? "lambda" : t.guard.replaceAll("[\\n\\r]+", " ").trim())
: 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("\"");
sb.append("/>\n");
// If choice/junction and order is set, add 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");
}
// 3. Add final states (if not already defined as states)
for (String end : endStates) {
String simpleEnd = simplify(end);
if (!allStates.contains(end)) {
sb.append(" <final id=\"").append(simpleEnd).append("\"/>\n\n");
}
sb.append(" </state>\n\n");
}
sb.append("</scxml>\n");
return sb.toString();
}
private static String escapeXml(String s) {
if (s == null) return "";
return s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
// Helper: extract last enum part
private static String simplify(String full) {
if (full == null) return "";
@@ -259,4 +344,10 @@ public class ExportUtils {
return dot >= 0 ? full.substring(dot + 1) : full;
}
private static String escapeXml(String s) {
if (s == null) return "";
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace("\"", "&quot;").replace("'", "&apos;");
}
}

View File

@@ -0,0 +1,68 @@
package com.example.statemachinedemo;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
import static com.example.statemachinedemo.AstFileFinder.hasClassWithAnnotation;
import static com.example.statemachinedemo.ExportUtils.*;
public class Main {
private static final String START_DIR = ".";
private static final String EXTENSION = ".java";
private static final String TARGET_ANNOTATION = "EnableStateMachineFactory";
public static void main(String[] args) throws IOException {
List<Path> javaFiles = AstFileFinder.findJavaFiles(Paths.get(START_DIR), EXTENSION);
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
// Check for annotation (optional)
if (hasClassWithAnnotation(source, TARGET_ANNOTATION)) {
System.out.println("Found annotation in: " + javaFile.toAbsolutePath());
}
// Find configure method using the reusable finder
MethodDeclaration configureMethod = StateMachineConfigureMethodFinder.findConfigureMethod(source);
if (configureMethod != null) {
System.out.println("Found configure method in: " + javaFile.toAbsolutePath());
// Print method body
System.out.println("Method body:\n" + configureMethod.getBody());
// Parse transitions
List<Transition> transitions = TransitionParser.parseTransitions(configureMethod);
for (Transition t : transitions) {
System.out.println(t);
}
// Find start and end states
Set<String> startStates = StateUtils.findStartStates(transitions);
Set<String> endStates = StateUtils.findEndStates(transitions);
System.out.println("Start States: " + startStates);
System.out.println("End States: " + endStates);
// Export outputs
String plantUML = generatePlantUML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.2.dot")) {
out.println(plantUML);
}
String dot = generateDot(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.1.dot")) {
out.println(dot);
}
String scxml = exportToSCXML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.3.xml")) {
out.println(scxml);
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
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;
}
}
}

View File

@@ -2,9 +2,7 @@ package com.example.statemachinedemo;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
public class StateUtils {
public static Set<String> findStartStates(List<Transition> transitions) {

View File

@@ -0,0 +1,25 @@
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 String sourceState;
public String targetState;
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
}

View File

@@ -1,35 +1,13 @@
package com.example.statemachinedemo;
import lombok.ToString;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
/**
* Parses Spring State Machine transitions from configure method AST.
*/
class Transition {
public String type; // withExternal, withChoice, withInternal, etc.
public String sourceState;
public String targetState;
public String event;
public String guard; // simplified string form, could be anonymous class, etc.
@Override
public String toString() {
return "Transition{" +
"type='" + type + '\'' +
", sourceState='" + sourceState + '\'' +
", targetState='" + targetState + '\'' +
", event='" + event + '\'' +
", guard='" + guard + '\'' +
'}';
}
}
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<>();
@@ -83,8 +61,11 @@ public class TransitionParser {
boolean isChoice = segment.stream()
.anyMatch(mi -> "withChoice".equals(mi.getName().getIdentifier()));
if (isChoice) {
transitions.addAll(parseChoiceTransitions(segment));
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));
}
@@ -93,9 +74,10 @@ public class TransitionParser {
return transitions;
}
private static List<Transition> parseChoiceTransitions(List<MethodInvocation> segment) {
List<Transition> choiceTransitions = new ArrayList<>();
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();
@@ -109,33 +91,64 @@ public class TransitionParser {
String name = call.getName().getIdentifier();
List<?> args = call.arguments();
if (name.equals("first") || name.equals("then") || name.equals("last")) {
if ("first".equals(name) || "then".equals(name) || "last".equals(name)) {
if (args.isEmpty()) continue;
Transition t = new Transition();
t.type = "withChoice";
t.type = type;
t.sourceState = sourceState;
t.targetState = 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);
}
choiceTransitions.add(t);
// 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 choiceTransitions;
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"
"withChoice",
"withJunction"
);
private static Transition parseStandardTransition(List<MethodInvocation> segment) {
@@ -164,6 +177,14 @@ public class TransitionParser {
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;
}
@@ -176,4 +197,4 @@ public class TransitionParser {
return t;
}
}
}

View File

@@ -3,6 +3,7 @@ package com.example.statemachinedemo.statemachine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
@@ -19,6 +20,7 @@ class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter<
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
Guard<OrderStates, OrderEvents> guard1 = null;
Action<OrderStates, OrderEvents> action2 = null;
transitions
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY)
.and()
@@ -68,7 +70,12 @@ class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter<
.and().withExternal().source(OrderStates.PAID2).target(OrderStates.CANCELED)
.and().withExternal().source(OrderStates.PAID3).target(OrderStates.CANCELED)
// .and().withLocal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELED)
.and().withExternal().source(OrderStates.PAID1).event(OrderEvents.ABCD);
.and().withExternal().source(OrderStates.PAID1).event(OrderEvents.ABCD).action(new Action<OrderStates, OrderEvents>() {
@Override
public void execute(StateContext<OrderStates, OrderEvents> context) {
;
}
}).action(action2);
}
@Override

View File

@@ -25,14 +25,14 @@ digraph stateMachine {
SUBMITTED -> CANCELED [label="CANCEL [lambda]"];
PAID -> CANCELED [label="CANCEL"];
SUBMITTED -> SUBMITTED [label="ABCD"];
SUBMITTED_choice -> PAID2 [label=" [lambda]"];
SUBMITTED_choice -> PAID3;
PAID_choice -> PAID1 [label=" [lambda]"];
PAID_choice -> PAID2 [label=" [lambda]"];
PAID_choice -> HAPPEN [label=" [lambda]"];
PAID_choice -> PAID3;
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"];
PAID1 -> PAID1 [label="ABCD / lambda, action2"];
}

View File

@@ -10,20 +10,20 @@ PAID --> PAID_choice
state SUBMITTED_choice <<choice>>
SUBMITTED --> SUBMITTED_choice
SUBMITTED --> PAID : PAY
PAID --> FULFILLED : FULFILL
SUBMITTED --> CANCELED : CANCEL [lambda]
PAID --> CANCELED : CANCEL
SUBMITTED --> SUBMITTED : ABCD
SUBMITTED_choice --> PAID2 : [lambda]
SUBMITTED_choice --> PAID3
PAID_choice --> PAID1 : [lambda]
PAID_choice --> PAID2 : [lambda]
PAID_choice --> HAPPEN : [lambda]
PAID_choice --> PAID3
PAID2 --> CANCELED
PAID3 --> CANCELED
PAID1 --> PAID1 : ABCD
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 --> [*]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 72 KiB

58
statemachine.3.xml Normal file
View File

@@ -0,0 +1,58 @@
<?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>

14
statemachine.dot Normal file
View File

@@ -0,0 +1,14 @@
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=""];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 122 KiB

11
statemachine.uml.dot Normal file
View File

@@ -0,0 +1,11 @@
@startuml
[*] --> SUBMITTED
SUBMITTED --> PAID : PAY
PAID --> FULFILLED : FULFILL
SUBMITTED --> CANCELED : CANCEL [guard]
PAID --> CANCELED : CANCEL
PAID2 --> CANCELED
PAID3 --> CANCELED
CANCELED --> [*]
FULFILLED --> [*]
@enduml