diff --git a/src/main/java/com/example/statemachinedemo/AstFileFinder.java b/src/main/java/com/example/statemachinedemo/AstFileFinder.java deleted file mode 100644 index 7fffb58..0000000 --- a/src/main/java/com/example/statemachinedemo/AstFileFinder.java +++ /dev/null @@ -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 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 deleted file mode 100644 index b2cb83b..0000000 --- a/src/main/java/com/example/statemachinedemo/ExportUtils.java +++ /dev/null @@ -1,426 +0,0 @@ -package com.example.statemachinedemo; - -import java.util.*; - -public class ExportUtils { - - public static String generatePlantUML(List transitions, - Set startStates, - Set endStates, - boolean useLambdaGuards) { - StringBuilder sb = new StringBuilder(); - sb.append("@startuml\n"); - sb.append("skinparam state {\n"); - sb.append(" BackgroundColor<> LightYellow\n"); - sb.append("}\n\n"); - - // 1. Print start states - for (String start : startStates) { - sb.append("[*] --> ").append(simplify(start)).append("\n"); - } - sb.append("\n"); - - // 2. Find all states that have choice transitions originating from them - Set statesWithChoice = new HashSet<>(); - for (Transition t : transitions) { - if ("withChoice".equals(t.type)) { - for (String source : t.sourceStates) { - statesWithChoice.add(simplify(source)); - } - } - } - - // 3. Declare choice pseudostates & connect from original state - for (String state : statesWithChoice) { - String choiceState = state + "_choice"; - sb.append("state ").append(choiceState).append(" <>\n"); - sb.append(state).append(" --> ").append(choiceState).append("\n"); - } - sb.append("\n"); - - // 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<>(); - - // Helper to get color for non-choice transitions (NO leading '#') - java.util.function.Function 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 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 transitions, - Set startStates, - Set 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 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 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 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 transitions, - Set startStates, - Set endStates, - boolean useLambdaGuards) { - StringBuilder sb = new StringBuilder(); - sb.append("\n"); - sb.append("\n\n"); - - // 1. Collect all states - Set 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 elements with transitions - for (String state : allStates) { - String simpleState = simplify(state); - sb.append(" \n"); - - // If this state is a start state, add and transition to it - if (startStates.contains(state)) { - sb.append(" \n"); - sb.append(" \n"); - sb.append(" \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 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(" \n"); - sb.append(" \n"); - sb.append(" \n"); - } else { - sb.append("/>\n"); - } - } - } - - sb.append(" \n\n"); - } - - sb.append("\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("'", "'"); - } - -} diff --git a/src/main/java/com/example/statemachinedemo/Main.java b/src/main/java/com/example/statemachinedemo/Main.java index ebb5fa1..0898f1d 100644 --- a/src/main/java/com/example/statemachinedemo/Main.java +++ b/src/main/java/com/example/statemachinedemo/Main.java @@ -1,5 +1,16 @@ 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.MethodDeclaration; @@ -12,75 +23,66 @@ import java.util.HashSet; 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); + List javaFiles = FileUtils.findFilesWithExtension(Paths.get(START_DIR), EXTENSION); + + // List of output handlers + List outputs = List.of( + new PlantUml(), + new Dot(), + new Scxml() + ); for (Path javaFile : javaFiles) { String source = Files.readString(javaFile); + AstFileFinder finder = new AstFileFinder(source); - // Check for annotation (optional) - if (hasClassWithAnnotation(source, TARGET_ANNOTATION)) { + if (finder.hasClassWithAnnotation(source, TARGET_ANNOTATION)) { System.out.println("Found annotation in: " + javaFile.toAbsolutePath()); } - // Find configure method using the reusable finder - MethodDeclaration configureMethod = StateMachineConfigureMethodFinder.findConfigureMethod(source); + MethodDeclaration configureMethod = new StateMachineTransitionConfigurationMethodFinder(source).findConfigureMethod(); 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); + List transitions = AstTransitionParser.parseTransitions(configureMethod); for (Transition t : transitions) { System.out.println(t); } - // Find start and end states - Set startStatesTransitions = StateUtils.findStartStates(transitions); - Set endStatesTransitions = StateUtils.findEndStates(transitions); + Set startStatesTransitions = TransitionStateUtils.findStartStates(transitions); + Set endStatesTransitions = TransitionStateUtils.findEndStates(transitions); - - CompilationUnit cu = AstStateUtils.parse(source); - AstStateUtils.StateConfigurerVisitor visitor = new AstStateUtils.StateConfigurerVisitor(); + CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source); + StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor = new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor(); cu.accept(visitor); + System.out.println("Start States Ast: " + visitor.getInitialStates()); System.out.println("End States Ast: " + visitor.getEndStates()); - System.out.println("Start States: " + startStatesTransitions); System.out.println("End States: " + endStatesTransitions); - Set startStates = new HashSet<>(startStatesTransitions); // copy set1 - startStates.addAll(visitor.getInitialStates()); // add all from set2 - Set endStates = new HashSet<>(endStatesTransitions); // copy set1 - endStates.addAll(visitor.getEndStates()); // add all from set2 + Set startStates = new HashSet<>(startStatesTransitions); + startStates.addAll(visitor.getInitialStates()); + + Set endStates = new HashSet<>(endStatesTransitions); + endStates.addAll(visitor.getEndStates()); - // Derive output base name from input file name String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", ""); - // Export outputs using dynamic file names - String plantUML = generatePlantUML(transitions, startStates, endStates, true); - try (PrintWriter out = new PrintWriter(baseName + ".plantuml.dot")) { - out.println(plantUML); - } - - 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); + // Iterate over outputs and generate files dynamically + for (StateMachineExporter output : outputs) { + String content = output.export(transitions, startStates, endStates, true); + String fileName = baseName + output.getFileExtension(); + try (PrintWriter out = new PrintWriter(fileName)) { + out.println(content); + } } } } diff --git a/src/main/java/com/example/statemachinedemo/StateMachineConfigureMethodFinder.java b/src/main/java/com/example/statemachinedemo/StateMachineConfigureMethodFinder.java deleted file mode 100644 index dec2c5a..0000000 --- a/src/main/java/com/example/statemachinedemo/StateMachineConfigureMethodFinder.java +++ /dev/null @@ -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 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 deleted file mode 100644 index 7869ef8..0000000 --- a/src/main/java/com/example/statemachinedemo/StateUtils.java +++ /dev/null @@ -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 findStartStates(List transitions) { - Set sources = new HashSet<>(); - Set 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 findEndStates(List transitions) { - Set targets = new HashSet<>(); - Set 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("^\"|\"$", ""); - } -} diff --git a/src/main/java/com/example/statemachinedemo/Transition.java b/src/main/java/com/example/statemachinedemo/Transition.java deleted file mode 100644 index 6b4a884..0000000 --- a/src/main/java/com/example/statemachinedemo/Transition.java +++ /dev/null @@ -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 sourceStates = new ArrayList<>(); - public List targetStates = new ArrayList<>(); - 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 deleted file mode 100644 index 16fda5e..0000000 --- a/src/main/java/com/example/statemachinedemo/TransitionParser.java +++ /dev/null @@ -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 parseTransitions(MethodDeclaration method) { - List 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 parseTransitionsFromExpression(MethodInvocation rootCall) { - List transitions = new ArrayList<>(); - - // Step 1: Unroll method chain into ordered list - List 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> segments = new ArrayList<>(); - List 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 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 parseChoiceOrJunctionTransitions(List segment, String type) { - List 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 SUPPORTED_TRANSITION_TYPES = Set.of( - "withExternal", - "withInternal", - "withLocal", - "withFork", - "withJoin", - "withChoice", - "withJunction" - ); - - private static Transition parseStandardTransition(List 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; - } -} diff --git a/src/main/java/com/example/statemachinedemo/app/AstTransitionParser.java b/src/main/java/com/example/statemachinedemo/app/AstTransitionParser.java new file mode 100644 index 0000000..d4951b2 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/app/AstTransitionParser.java @@ -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 SUPPORTED_TRANSITION_TYPES = Set.of( + "withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction" + ); + + public static List parseTransitions(MethodDeclaration method) { + List 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 parseTransitionsFromExpression(MethodInvocation rootCall) { + List transitions = new ArrayList<>(); + + // Step 1: Unroll method chain + List calls = new ArrayList<>(); + Expression current = rootCall; + while (current instanceof MethodInvocation mi) { + calls.addFirst(mi); + current = mi.getExpression(); + } + + // Step 2: Split by .and() + List> segments = new ArrayList<>(); + List 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 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 parseChoiceOrJunctionTransitions(List segment, String type) { + List 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 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; + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/AstStateUtils.java b/src/main/java/com/example/statemachinedemo/app/StateMachineStateConfigurationMethodFinder.java similarity index 55% rename from src/main/java/com/example/statemachinedemo/AstStateUtils.java rename to src/main/java/com/example/statemachinedemo/app/StateMachineStateConfigurationMethodFinder.java index e91fa43..1f36a61 100644 --- a/src/main/java/com/example/statemachinedemo/AstStateUtils.java +++ b/src/main/java/com/example/statemachinedemo/app/StateMachineStateConfigurationMethodFinder.java @@ -1,23 +1,28 @@ -package com.example.statemachinedemo; +package com.example.statemachinedemo.app; +import lombok.Getter; 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 { - private Set initialStates = new HashSet<>(); - private Set endStates = new HashSet<>(); - - public Set getInitialStates() { - return initialStates; - } - - public Set getEndStates() { - return endStates; - } + private final Set initialStates = new HashSet<>(); + private final Set endStates = new HashSet<>(); @Override public boolean visit(MethodDeclaration node) { @@ -31,13 +36,9 @@ public class AstStateUtils { // Traverse statements inside configure method for (Object stmtObj : body.statements()) { - if (!(stmtObj instanceof ExpressionStatement)) continue; - - ExpressionStatement exprStmt = (ExpressionStatement) stmtObj; + if (!(stmtObj instanceof ExpressionStatement exprStmt)) continue; Expression expr = exprStmt.getExpression(); - - if (expr instanceof MethodInvocation) { - MethodInvocation rootCall = (MethodInvocation) expr; + if (expr instanceof MethodInvocation rootCall) { // Check if this chain starts with "withStates" if (isWithStatesChain(rootCall)) { @@ -50,26 +51,24 @@ public class AstStateUtils { private boolean isWithStatesChain(MethodInvocation mi) { MethodInvocation current = mi; - while (current != null) { + while (true) { + Expression expr = current.getExpression(); if ("withStates".equals(current.getName().getIdentifier())) { return true; } - Expression expr = current.getExpression(); - if (expr instanceof MethodInvocation) { - current = (MethodInvocation) expr; + if (expr instanceof MethodInvocation next) { + current = next; } else { - break; + return false; } } - return false; } private boolean isInsideRegionOrFork(MethodInvocation mi) { Expression expr = mi.getExpression(); - while (expr instanceof MethodInvocation) { - MethodInvocation parent = (MethodInvocation) expr; - String name = parent.getName().getIdentifier(); - if ("fork".equals(name) || "region".equals(name)) { + while (expr instanceof MethodInvocation parent) { + String methodName = parent.getName().getIdentifier(); + if (List.of("fork", "region").contains(methodName)) { return true; } expr = parent.getExpression(); @@ -80,37 +79,28 @@ public class AstStateUtils { private void parseMethodChain(MethodInvocation mi) { List chain = new ArrayList<>(); Expression current = mi; - while (current instanceof MethodInvocation) { - MethodInvocation m = (MethodInvocation) current; - chain.add(0, m); + while (current instanceof MethodInvocation m) { + chain.addFirst(m); current = m.getExpression(); } for (MethodInvocation call : chain) { String methodName = call.getName().getIdentifier(); List args = call.arguments(); - if (args.isEmpty()) continue; - - if ("initial".equals(methodName)) { - if (!isInsideRegionOrFork(call)) { - Expression arg = (Expression) args.get(0); - initialStates.add(arg.toString()); - } - } else if ("end".equals(methodName)) { - Expression arg = (Expression) args.get(0); - endStates.add(arg.toString()); + if (args.isEmpty()) { + continue; + } + Expression arg = (Expression) args.getFirst(); + switch (methodName) { + case "initial" -> { + if (!isInsideRegionOrFork(call)) { + initialStates.add(arg.toString()); + } + } + case "end" -> endStates.add(arg.toString()); + // ignore others } - // ignore other calls for extraction } } } - - public static CompilationUnit parse(String source) { - ASTParser parser = ASTParser.newParser(AST.JLS21); - parser.setSource(source.toCharArray()); - parser.setKind(ASTParser.K_COMPILATION_UNIT); - parser.setResolveBindings(false); - - return (CompilationUnit) parser.createAST(null); - } } diff --git a/src/main/java/com/example/statemachinedemo/app/StateMachineTransitionConfigurationMethodFinder.java b/src/main/java/com/example/statemachinedemo/app/StateMachineTransitionConfigurationMethodFinder.java new file mode 100644 index 0000000..5424a3d --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/app/StateMachineTransitionConfigurationMethodFinder.java @@ -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 params = method.parameters(); + if (params.size() != 1) + return false; + + if (!params.getFirst().getType().toString().startsWith("StateMachineTransitionConfigurer")) + return false; + + @SuppressWarnings("unchecked") + List thrownExceptions = method.thrownExceptionTypes(); + for (Type excType : thrownExceptions) { + if (excType.toString().equals("Exception")) { + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/app/TransitionStateUtils.java b/src/main/java/com/example/statemachinedemo/app/TransitionStateUtils.java new file mode 100644 index 0000000..a8a6294 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/app/TransitionStateUtils.java @@ -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 findStartStates(Collection transitions) { + Set 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 findEndStates(Collection transitions) { + Set 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 normalizeStates(Collection states) { + return states.stream() + .map(TransitionStateUtils::stripQuotes) + .filter(Objects::nonNull); + } + + private static String stripQuotes(String s) { + if (s == null) return null; + return s.replaceAll("^\"|\"$", ""); + } +} diff --git a/src/main/java/com/example/statemachinedemo/app/domain/Transition.java b/src/main/java/com/example/statemachinedemo/app/domain/Transition.java new file mode 100644 index 0000000..317c7bf --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/app/domain/Transition.java @@ -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 sourceStates = new ArrayList<>(); + private List targetStates = new ArrayList<>(); + private String event; + + private String guard; + private boolean isLambdaGuard = false; + + private List actions = new ArrayList<>(); + private List isLambdaActions = new ArrayList<>(); + + private Integer order = null; // optional order for withChoice or withJunction +} diff --git a/src/main/java/com/example/statemachinedemo/common/FileUtils.java b/src/main/java/com/example/statemachinedemo/common/FileUtils.java new file mode 100644 index 0000000..397e2d9 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/common/FileUtils.java @@ -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 findFilesWithExtension(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()); + } + } +} diff --git a/src/main/java/com/example/statemachinedemo/in/AstFileFinder.java b/src/main/java/com/example/statemachinedemo/in/AstFileFinder.java new file mode 100644 index 0000000..0f442fe --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/in/AstFileFinder.java @@ -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(); + } +} diff --git a/src/main/java/com/example/statemachinedemo/out/Dot.java b/src/main/java/com/example/statemachinedemo/out/Dot.java new file mode 100644 index 0000000..494b5a8 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/out/Dot.java @@ -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 transitions, + Set startStates, + Set 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 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 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 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"; + } +} diff --git a/src/main/java/com/example/statemachinedemo/out/PlantUml.java b/src/main/java/com/example/statemachinedemo/out/PlantUml.java new file mode 100644 index 0000000..066938f --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/out/PlantUml.java @@ -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 transitions, + Set startStates, + Set endStates, + boolean useLambdaGuards) { + StringBuilder sb = new StringBuilder(); + sb.append("@startuml\n"); + sb.append("skinparam state {\n"); + sb.append(" BackgroundColor<> LightYellow\n"); + sb.append("}\n\n"); + + // 1. Print start states + for (String start : startStates) { + sb.append("[*] --> ").append(simplify(start)).append("\n"); + } + sb.append("\n"); + + // 2. Find all states that have choice transitions originating from them + Set statesWithChoice = new HashSet<>(); + for (Transition t : transitions) { + if ("withChoice".equals(t.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(" <>\n"); + sb.append(state).append(" --> ").append(choiceState).append("\n"); + } + sb.append("\n"); + + // Color palette for choices + List choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4"); + + // Assign color to each choice state + 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 + for (Transition t : transitions) { + if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue; + + boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType()); + + List 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 actions = t.getActions(); + List 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 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 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"; + }; + } +} diff --git a/src/main/java/com/example/statemachinedemo/out/Scxml.java b/src/main/java/com/example/statemachinedemo/out/Scxml.java new file mode 100644 index 0000000..3a4b10f --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/out/Scxml.java @@ -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 transitions, + Set startStates, + Set endStates, + boolean useLambdaGuards) { + + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("\n\n"); + + // 1. Collect all states + Set 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 elements + for (String state : allStates) { + String simpleState = simplify(state); + sb.append(" \n"); + + if (startStates.contains(state)) { + sb.append(" \n"); + sb.append(" \n"); + sb.append(" \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 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(" \n\n"); + } + + sb.append("\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(" \n"); + sb.append(" \n"); + sb.append(" \n"); + } else { + sb.append("/>\n"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/com/example/statemachinedemo/out/StateMachineExporter.java b/src/main/java/com/example/statemachinedemo/out/StateMachineExporter.java new file mode 100644 index 0000000..3ee15d1 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/out/StateMachineExporter.java @@ -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 transitions, + Set startStates, + Set 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" +} diff --git a/src/main/java/com/example/statemachinedemo/notgoodspringbased/SpringExporter.java b/src/main/java/com/example/statemachinedemo/z/notgoodspringbased/SpringExporter.java similarity index 87% rename from src/main/java/com/example/statemachinedemo/notgoodspringbased/SpringExporter.java rename to src/main/java/com/example/statemachinedemo/z/notgoodspringbased/SpringExporter.java index 373fccb..978dc9c 100644 --- a/src/main/java/com/example/statemachinedemo/notgoodspringbased/SpringExporter.java +++ b/src/main/java/com/example/statemachinedemo/z/notgoodspringbased/SpringExporter.java @@ -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.statemachine.OrderStates; +import com.example.statemachinedemo.z.statemachine.OrderEvents; +import com.example.statemachinedemo.z.statemachine.OrderStates; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.statemachine.StateMachine; @@ -34,8 +35,7 @@ public class SpringExporter { @Slf4j @RequiredArgsConstructor class Runner implements ApplicationRunner { - private final StateMachineFactory factory; - // +// private final StateMachineFactory simpleEnumStateMachineConfiguration; // @Override // public void run(ApplicationArguments args) throws Exception { // StateMachine stateMachine = factory.getStateMachine("order"); @@ -51,7 +51,7 @@ class Runner implements ApplicationRunner { public void run(ApplicationArguments args) throws Exception { c.a(); c2.a(); -// c3.exportToSCXML(); + c3.exportToSCXML(); } } @@ -126,13 +126,13 @@ class StateMachinePlantUMLExporter { } } - @Component @RequiredArgsConstructor class Config1 { - private final StateMachineFactory factory; + @Qualifier("simpleEnumStateMachineFactory") + private final StateMachineFactory simpleEnumStateMachineConfiguration; public void a() throws FileNotFoundException { - StateMachine stateMachine = factory.getStateMachine("order"); + StateMachine stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order"); StateMachineDotExporter exporter = new StateMachineDotExporter(); try (PrintWriter out = new PrintWriter("statemachine.dot")) { @@ -144,9 +144,10 @@ class Config1 { @Component @RequiredArgsConstructor class Config2 { - private final StateMachineFactory factory; + @Qualifier("simpleEnumStateMachineFactory") + private final StateMachineFactory simpleEnumStateMachineConfiguration; public void a() throws FileNotFoundException { - StateMachine stateMachine = factory.getStateMachine("order"); + StateMachine stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order"); try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) { StateMachinePlantUMLExporter.export(stateMachine, out); @@ -156,12 +157,13 @@ class Config2 { @Component @RequiredArgsConstructor class Config3 { - private final StateMachineFactory factory; + @Qualifier("simpleEnumStateMachineFactory") + private final StateMachineFactory simpleEnumStateMachineConfiguration; public void exportToSCXML() throws Exception { - StateMachine stateMachine = factory.getStateMachine("order"); + StateMachine stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order"); SCXMLExporter exporter = new SCXMLExporter(); - File file = new File("statemachine.scxml"); + File file = new File("statemachine.scxml.xml"); exporter.exportToSCXML(stateMachine); } } diff --git a/src/main/java/com/example/statemachinedemo/statemachine/ComplexStateMachineConfig.java b/src/main/java/com/example/statemachinedemo/z/statemachine/ComplexStateMachineConfig.java similarity index 95% rename from src/main/java/com/example/statemachinedemo/statemachine/ComplexStateMachineConfig.java rename to src/main/java/com/example/statemachinedemo/z/statemachine/ComplexStateMachineConfig.java index ff58919..8515819 100644 --- a/src/main/java/com/example/statemachinedemo/statemachine/ComplexStateMachineConfig.java +++ b/src/main/java/com/example/statemachinedemo/z/statemachine/ComplexStateMachineConfig.java @@ -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.statemachine.StateContext; import org.springframework.statemachine.action.Action; -import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnableStateMachineFactory; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; -import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; @@ -15,7 +11,7 @@ import org.springframework.statemachine.guard.Guard; import java.util.EnumSet; @Configuration -@EnableStateMachineFactory +@EnableStateMachineFactory(name = "complexStateMachineFactory") public class ComplexStateMachineConfig extends StateMachineConfigurerAdapter { public enum States { diff --git a/src/main/java/com/example/statemachinedemo/statemachine/ForkJoinStateMachineConfig.java b/src/main/java/com/example/statemachinedemo/z/statemachine/ForkJoinStateMachineConfig.java similarity index 95% rename from src/main/java/com/example/statemachinedemo/statemachine/ForkJoinStateMachineConfig.java rename to src/main/java/com/example/statemachinedemo/z/statemachine/ForkJoinStateMachineConfig.java index f0a8f83..bd027f8 100644 --- a/src/main/java/com/example/statemachinedemo/statemachine/ForkJoinStateMachineConfig.java +++ b/src/main/java/com/example/statemachinedemo/z/statemachine/ForkJoinStateMachineConfig.java @@ -1,19 +1,16 @@ -package com.example.statemachinedemo.statemachine; +package com.example.statemachinedemo.z.statemachine; import org.springframework.context.annotation.Configuration; 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.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; -import org.springframework.statemachine.state.EnumState; import org.springframework.statemachine.guard.Guard; -import java.util.EnumSet; - @Configuration -@EnableStateMachine +@EnableStateMachineFactory(name = "forkJoinStateMachineFactory") public class ForkJoinStateMachineConfig extends StateMachineConfigurerAdapter { public enum States { diff --git a/src/main/java/com/example/statemachinedemo/statemachine/OrderEvents.java b/src/main/java/com/example/statemachinedemo/z/statemachine/OrderEvents.java similarity index 59% rename from src/main/java/com/example/statemachinedemo/statemachine/OrderEvents.java rename to src/main/java/com/example/statemachinedemo/z/statemachine/OrderEvents.java index 9cf826b..60f7402 100644 --- a/src/main/java/com/example/statemachinedemo/statemachine/OrderEvents.java +++ b/src/main/java/com/example/statemachinedemo/z/statemachine/OrderEvents.java @@ -1,4 +1,4 @@ -package com.example.statemachinedemo.statemachine; +package com.example.statemachinedemo.z.statemachine; public enum OrderEvents { FULFILL, PAY, CANCEL, diff --git a/src/main/java/com/example/statemachinedemo/statemachine/OrderStates.java b/src/main/java/com/example/statemachinedemo/z/statemachine/OrderStates.java similarity index 70% rename from src/main/java/com/example/statemachinedemo/statemachine/OrderStates.java rename to src/main/java/com/example/statemachinedemo/z/statemachine/OrderStates.java index 5d30ed3..fc78ddf 100644 --- a/src/main/java/com/example/statemachinedemo/statemachine/OrderStates.java +++ b/src/main/java/com/example/statemachinedemo/z/statemachine/OrderStates.java @@ -1,4 +1,4 @@ -package com.example.statemachinedemo.statemachine; +package com.example.statemachinedemo.z.statemachine; public enum OrderStates { SUBMITTED, PAID, FULFILLED, CANCELED diff --git a/src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java b/src/main/java/com/example/statemachinedemo/z/statemachine/SimpleEnumStateMachineConfiguration.java similarity index 97% rename from src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java rename to src/main/java/com/example/statemachinedemo/z/statemachine/SimpleEnumStateMachineConfiguration.java index 006c8ea..f90df8e 100644 --- a/src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java +++ b/src/main/java/com/example/statemachinedemo/z/statemachine/SimpleEnumStateMachineConfiguration.java @@ -1,4 +1,4 @@ -package com.example.statemachinedemo.statemachine; +package com.example.statemachinedemo.z.statemachine; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; @@ -14,7 +14,7 @@ import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.state.State; @Configuration -@EnableStateMachineFactory +@EnableStateMachineFactory(name = "simpleEnumStateMachineFactory") @Slf4j class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter { @Override diff --git a/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java b/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java index 41ff069..570c043 100644 --- a/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java +++ b/src/test/java/com/example/statemachinedemo/StatemachinedemoApplicationTests.java @@ -3,7 +3,7 @@ package com.example.statemachinedemo; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -//@SpringBootTest +@SpringBootTest class StatemachinedemoApplicationTests { @Test diff --git a/statemachine.1.dot b/statemachine.1.dot deleted file mode 100644 index 0b411cb..0000000 --- a/statemachine.1.dot +++ /dev/null @@ -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"]; -} - diff --git a/statemachine.2.dot b/statemachine.2.dot deleted file mode 100644 index fa3c4a1..0000000 --- a/statemachine.2.dot +++ /dev/null @@ -1,33 +0,0 @@ -@startuml -skinparam state { - BackgroundColor<> LightYellow -} - -[*] --> SUBMITTED - -state PAID_choice <> -PAID --> PAID_choice -state SUBMITTED_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 - diff --git a/statemachine.2.png b/statemachine.2.png deleted file mode 100644 index 5e6dbeb..0000000 Binary files a/statemachine.2.png and /dev/null differ diff --git a/statemachine.3.xml b/statemachine.3.xml deleted file mode 100644 index a1ab52f..0000000 --- a/statemachine.3.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/statemachine.dot b/statemachine.dot deleted file mode 100644 index d6e5604..0000000 --- a/statemachine.dot +++ /dev/null @@ -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=""]; -} diff --git a/statemachine.png b/statemachine.png deleted file mode 100644 index 46961a4..0000000 Binary files a/statemachine.png and /dev/null differ diff --git a/statemachine.uml.dot b/statemachine.uml.dot deleted file mode 100644 index 7afd62f..0000000 --- a/statemachine.uml.dot +++ /dev/null @@ -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