diff --git a/src/main/java/com/example/statemachinedemo/AFinder.java b/src/main/java/com/example/statemachinedemo/AFinder.java deleted file mode 100644 index 418bed2..0000000 --- a/src/main/java/com/example/statemachinedemo/AFinder.java +++ /dev/null @@ -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 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 transitions = TransitionParser.parseTransitions(configureMethod); - for (Transition t : transitions) { - System.out.println(t); - } - // Find start and end states - Set startStates = StateUtils.findStartStates(transitions); - Set endStates = StateUtils.findEndStates(transitions); - System.out.println("Start States: " + startStates); - System.out.println("End States: " + endStates); - - - 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 findJavaFiles(Path startDir) throws IOException { - try (Stream 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 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 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 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 - 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 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; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/AstFileFinder.java b/src/main/java/com/example/statemachinedemo/AstFileFinder.java new file mode 100644 index 0000000..7fffb58 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/AstFileFinder.java @@ -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 findJavaFiles(Path startDir, String extension) throws IOException { + try (Stream 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]; + } +} diff --git a/src/main/java/com/example/statemachinedemo/ExportUtils.java b/src/main/java/com/example/statemachinedemo/ExportUtils.java index 974c89f..db1a33e 100644 --- a/src/main/java/com/example/statemachinedemo/ExportUtils.java +++ b/src/main/java/com/example/statemachinedemo/ExportUtils.java @@ -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 transitions, - Set startStates, - Set endStates, - boolean useLambdaGuards) { + Set startStates, + Set 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 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 choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4"); + + // Assign a color per withChoice source state cycling through palette + Map choiceStateColorMap = new HashMap<>(); + int colorIndex = 0; + for (String state : statesWithChoice) { + choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size())); + colorIndex++; + } + Set transitionLines = new HashSet<>(); - // 4. Output transitions: + // Helper to get color for non-choice transitions (NO leading '#') + java.util.function.Function getTransitionColor = (type) -> { + if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue + if ("withInternal".equals(type)) return "32CD32"; // LimeGreen + if ("withJunction".equals(type)) return "FF69B4"; // HotPink + return "000000"; // Black fallback + }; + + // 4. Output transitions for (Transition t : transitions) { if (t.sourceState == null || t.targetState == null) continue; @@ -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 transitions, Set startStates, Set 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(" \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(" \n"); + sb.append(" \n"); + } else { + sb.append("/>\n"); + } } } - sb.append(" \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(" \n\n"); - } + sb.append(" \n\n"); } sb.append("\n"); return sb.toString(); } - private static String escapeXml(String s) { - if (s == null) return ""; - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - - // 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("&", "&").replace("<", "<").replace(">", ">") + .replace("\"", """).replace("'", "'"); + } + } diff --git a/src/main/java/com/example/statemachinedemo/Main.java b/src/main/java/com/example/statemachinedemo/Main.java new file mode 100644 index 0000000..050053d --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/Main.java @@ -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 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 transitions = TransitionParser.parseTransitions(configureMethod); + for (Transition t : transitions) { + System.out.println(t); + } + + // Find start and end states + Set startStates = StateUtils.findStartStates(transitions); + Set endStates = StateUtils.findEndStates(transitions); + System.out.println("Start States: " + startStates); + System.out.println("End States: " + endStates); + + // 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); + } + } + } + } +} diff --git a/src/main/java/com/example/statemachinedemo/StateMachineConfigureMethodFinder.java b/src/main/java/com/example/statemachinedemo/StateMachineConfigureMethodFinder.java new file mode 100644 index 0000000..dec2c5a --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/StateMachineConfigureMethodFinder.java @@ -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 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 + 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 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; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/StateUtils.java b/src/main/java/com/example/statemachinedemo/StateUtils.java index 9d458b4..77d0e9e 100644 --- a/src/main/java/com/example/statemachinedemo/StateUtils.java +++ b/src/main/java/com/example/statemachinedemo/StateUtils.java @@ -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 findStartStates(List transitions) { diff --git a/src/main/java/com/example/statemachinedemo/Transition.java b/src/main/java/com/example/statemachinedemo/Transition.java new file mode 100644 index 0000000..c4c6931 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/Transition.java @@ -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 actions = new ArrayList<>(); + public List isLambdaActions = new ArrayList<>(); + + public Integer order = null; // optional order for withChoice or withJunction +} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/TransitionParser.java b/src/main/java/com/example/statemachinedemo/TransitionParser.java index 2e7b413..1243d66 100644 --- a/src/main/java/com/example/statemachinedemo/TransitionParser.java +++ b/src/main/java/com/example/statemachinedemo/TransitionParser.java @@ -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 parseTransitions(MethodDeclaration method) { List 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 parseChoiceTransitions(List segment) { - List choiceTransitions = new ArrayList<>(); + private static List parseChoiceOrJunctionTransitions(List segment, String type) { + List 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 SUPPORTED_TRANSITION_TYPES = Set.of( "withExternal", "withInternal", "withLocal", "withFork", "withJoin", - "withChoice" + "withChoice", + "withJunction" ); private static Transition parseStandardTransition(List 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; } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java b/src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java index 7cc6141..006c8ea 100644 --- a/src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java +++ b/src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java @@ -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 transitions) throws Exception { Guard guard1 = null; + Action 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() { + @Override + public void execute(StateContext context) { + ; + } + }).action(action2); } @Override diff --git a/statemachine.1.dot b/statemachine.1.dot index 3a96d98..0b411cb 100644 --- a/statemachine.1.dot +++ b/statemachine.1.dot @@ -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"]; } diff --git a/statemachine.2.dot b/statemachine.2.dot index c0518c9..fa3c4a1 100644 --- a/statemachine.2.dot +++ b/statemachine.2.dot @@ -10,20 +10,20 @@ PAID --> PAID_choice state SUBMITTED_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 --> [*] diff --git a/statemachine.2.png b/statemachine.2.png index 0a9c97a..5e6dbeb 100644 Binary files a/statemachine.2.png and b/statemachine.2.png differ diff --git a/statemachine.3.xml b/statemachine.3.xml new file mode 100644 index 0000000..a1ab52f --- /dev/null +++ b/statemachine.3.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/statemachine.dot b/statemachine.dot new file mode 100644 index 0000000..d6e5604 --- /dev/null +++ b/statemachine.dot @@ -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=""]; +} diff --git a/statemachine.png b/statemachine.png index 910b84e..46961a4 100644 Binary files a/statemachine.png and b/statemachine.png differ diff --git a/statemachine.uml.dot b/statemachine.uml.dot new file mode 100644 index 0000000..7afd62f --- /dev/null +++ b/statemachine.uml.dot @@ -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