diff --git a/build.gradle b/build.gradle index c3207f2..5a0ab8a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,11 @@ dependencies { // deps for SCXMLWriter // implementation "org.apache.commons:scxml4j:1.0.0" + implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.36.0' + + // deps for parsing java AST + implementation 'com.github.javaparser:javaparser-core:3.27.0' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/example/statemachinedemo/AFinder.java b/src/main/java/com/example/statemachinedemo/AFinder.java new file mode 100644 index 0000000..418bed2 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/AFinder.java @@ -0,0 +1,394 @@ +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/ExportUtils.java b/src/main/java/com/example/statemachinedemo/ExportUtils.java new file mode 100644 index 0000000..974c89f --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/ExportUtils.java @@ -0,0 +1,262 @@ +package com.example.statemachinedemo; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +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 + // We'll reroute those transitions through a separate choice pseudostate. + Set statesWithChoice = new HashSet<>(); + for (Transition t : transitions) { + if ("withChoice".equals(t.type)) { + statesWithChoice.add(simplify(t.sourceState)); + } + } + + // 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"); + + Set transitionLines = new HashSet<>(); + + // 4. Output transitions: + for (Transition t : transitions) { + if (t.sourceState == null || t.targetState == null) continue; + + String source = simplify(t.sourceState); + String target = simplify(t.targetState); + + 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()) { + guardText = useLambdaGuards ? "lambda" + : t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim(); + } + + // Build label with event and guard + StringBuilder label = new StringBuilder(); + if (t.event != null && !t.event.isBlank()) { + label.append(simplify(t.event)); + } + if (!guardText.isBlank()) { + label.append(" [").append(guardText).append("]"); + } + + String line = transitionSource + " --> " + 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)) { + statesWithChoice.add(simplify(t.sourceState)); + } + } + + // 3. Declare normal states and choice pseudostates + Set allStates = new HashSet<>(); + for (Transition t : transitions) { + allStates.add(simplify(t.sourceState)); + allStates.add(simplify(t.targetState)); + } + + // 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.sourceState == null || t.targetState == null) continue; + + String source = simplify(t.sourceState); + String target = simplify(t.targetState); + String edgeSource = source; + + if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) { + edgeSource = source + "_choice"; + } + + String guardText = ""; + if (t.guard != null && !t.guard.isBlank()) { + guardText = useLambdaGuards ? "lambda" + : t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim(); + } + + StringBuilder label = new StringBuilder(); + if (t.event != null && !t.event.isBlank()) { + label.append(simplify(t.event)); + } + if (!guardText.isBlank()) { + label.append(" [").append(guardText).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.sourceState != null) allStates.add(t.sourceState); + if (t.targetState != null) allStates.add(t.targetState); + } + + // 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 (state.equals(t.sourceState)) { + String target = simplify(t.targetState); + 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; + + 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"); + 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 ""; + int dot = full.lastIndexOf('.'); + return dot >= 0 ? full.substring(dot + 1) : full; + } + +} diff --git a/src/main/java/com/example/statemachinedemo/StateUtils.java b/src/main/java/com/example/statemachinedemo/StateUtils.java new file mode 100644 index 0000000..9d458b4 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/StateUtils.java @@ -0,0 +1,52 @@ +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) { + Set sources = new HashSet<>(); + Set targets = new HashSet<>(); + + for (Transition t : transitions) { + String source = stripQuotes(t.sourceState); + String target = stripQuotes(t.targetState); + + if (source != null) sources.add(source); + if (target != null && !target.equals(source)) { + targets.add(target); + } + } + + // Start states = sources that are not a target (except self-targets allowed) + sources.removeAll(targets); + return sources; + } + + public static Set findEndStates(List transitions) { + Set targets = new HashSet<>(); + Set sources = new HashSet<>(); + + for (Transition t : transitions) { + String source = stripQuotes(t.sourceState); + String target = stripQuotes(t.targetState); + + if (target != null) targets.add(target); + if (source != null && !source.equals(target)) { + sources.add(source); + } + } + + // End states = targets that are not a source (except self-source allowed) + 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/StatemachinedemoApplication.java b/src/main/java/com/example/statemachinedemo/StatemachinedemoApplication.java index 8ba4326..1690bd1 100644 --- a/src/main/java/com/example/statemachinedemo/StatemachinedemoApplication.java +++ b/src/main/java/com/example/statemachinedemo/StatemachinedemoApplication.java @@ -1,274 +1,11 @@ package com.example.statemachinedemo; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Configuration; -import org.springframework.statemachine.StateContext; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.config.EnableStateMachineFactory; -import org.springframework.statemachine.config.StateMachineConfigurerAdapter; -import org.springframework.statemachine.config.StateMachineFactory; -import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; -import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; -import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; -import org.springframework.statemachine.guard.Guard; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; -import org.springframework.statemachine.state.PseudoStateKind; -import org.springframework.statemachine.state.State; -import org.springframework.statemachine.transition.Transition; -import org.springframework.stereotype.Component; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.PrintWriter; -import java.util.Collection; @SpringBootApplication public class StatemachinedemoApplication { public static void main(String[] args) { SpringApplication.run(StatemachinedemoApplication.class, args); } -} - -enum OrderEvents { - FULFILL, PAY, CANCEL -} - -enum OrderStates { - SUBMITTED, PAID, FULFILLED, CANCELED -// ,PAID2, PAID3 -} - -@Component -@Slf4j -@RequiredArgsConstructor -class Runner implements ApplicationRunner { - private final StateMachineFactory factory; -// -// @Override -// public void run(ApplicationArguments args) throws Exception { -// StateMachine stateMachine = factory.getStateMachine("order"); -// stateMachine.start(); -// log.info("Current state is {}", stateMachine.getState().getId()); -// -// } - private final Config1 c; - private final Config2 c2; - private final Config3 c3; - - @Override - public void run(ApplicationArguments args) throws Exception { - c.a(); - c2.a(); -// c3.exportToSCXML(); - } -} - -class StateMachineDotExporter { - public static void export(StateMachine stateMachine, PrintWriter writer) { - writer.println("digraph stateMachine {"); - - for (State state : stateMachine.getStates()) { - writer.printf(" %s;%n", state.getId()); - } - - for (Transition transition : stateMachine.getTransitions()) { - if (transition.getSource() != null && transition.getTarget() != null) { - writer.printf(" %s -> %s [label=\"%s\"];%n", - transition.getSource().getId(), - transition.getTarget().getId(), - transition.getTrigger() != null ? transition.getTrigger().getEvent() : ""); - } - } - - writer.println("}"); - } -} - -class StateMachinePlantUMLExporter { - - public static void export(StateMachine stateMachine, PrintWriter writer) { - writer.println("@startuml"); - writer.println("[*] --> " + stateMachine.getInitialState().getId()); - - for (Transition transition : stateMachine.getTransitions()) { - if (transition.getSource() != null && transition.getTarget() != null) { - String source = transition.getSource().getId().toString(); - String target = transition.getTarget().getId().toString(); - String event = transition.getTrigger() != null && transition.getTrigger().getEvent() != null - ? transition.getTrigger().getEvent().toString() - : ""; - writer.printf("%s --> %s : %s%n", source, target, event); - } - } - - for (State state : stateMachine.getStates()) { - if (state.getPseudoState() != null && - state.getPseudoState().getKind() == PseudoStateKind.END) { - writer.printf("%s --> [*]%n", state.getId()); - } - } - writer.println("@enduml"); - } -} - -@Component -@RequiredArgsConstructor -class Config1 { - private final StateMachineFactory factory; - public void a() throws FileNotFoundException { - StateMachine stateMachine = factory.getStateMachine("order"); - - StateMachineDotExporter exporter = new StateMachineDotExporter(); - try (PrintWriter out = new PrintWriter("statemachine.dot")) { - StateMachineDotExporter.export(stateMachine, out); - } - } -} - -@Component -@RequiredArgsConstructor -class Config2 { - private final StateMachineFactory factory; - public void a() throws FileNotFoundException { - StateMachine stateMachine = factory.getStateMachine("order"); - - try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) { - StateMachinePlantUMLExporter.export(stateMachine, out); - } - } -} - -@Component -class SCXMLExporter { - - // This method assumes you already have a state machine - public void exportToSCXML(StateMachine stateMachine) throws Exception { - // Create a document to represent the SCXML structure - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.newDocument(); - - // Root SCXML element - Element scxmlElement = doc.createElement("scxml"); - scxmlElement.setAttribute("version", "1.0"); - doc.appendChild(scxmlElement); - - // Define the states - Element statesElement = doc.createElement("states"); - scxmlElement.appendChild(statesElement); - - // Iterate over the states of the state machine - Collection> states = stateMachine.getStates(); - for (State state : states) { - Element stateElement = doc.createElement("state"); - stateElement.setAttribute("id", state.getId().toString()); - statesElement.appendChild(stateElement); - } - - // Define the transitions - Element transitionsElement = doc.createElement("transitions"); - scxmlElement.appendChild(transitionsElement); - - Collection> transitions = stateMachine.getTransitions(); - for (Transition transition : transitions) { - Element transitionElement = doc.createElement("transition"); - transitionElement.setAttribute("from", transition.getSource().getId().toString()); - transitionElement.setAttribute("to", transition.getTarget().getId().toString()); - transitionElement.setAttribute("event", String.valueOf(transition.getTrigger())); - transitionsElement.appendChild(transitionElement); - } - - // Write to file - File file = new File("stateMachine.scxml"); - saveDocumentToFile(doc, file); - } - - private void saveDocumentToFile(Document doc, File file) throws Exception { - // Serialize the document to a file (you can use your preferred XML writer) - javax.xml.transform.Transformer transformer = javax.xml.transform.TransformerFactory.newInstance().newTransformer(); - javax.xml.transform.stream.StreamResult result = new javax.xml.transform.stream.StreamResult(file); - javax.xml.transform.dom.DOMSource source = new javax.xml.transform.dom.DOMSource(doc); - transformer.transform(source, result); - } -} -@Component -@RequiredArgsConstructor -class Config3 { - private final StateMachineFactory factory; - public void exportToSCXML() throws Exception { - StateMachine stateMachine = factory.getStateMachine("order"); - - SCXMLExporter exporter = new SCXMLExporter(); - File file = new File("statemachine.scxml"); - exporter.exportToSCXML(stateMachine); - } -} - -@Configuration -@EnableStateMachineFactory -@Slf4j -class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter { - @Override - public void configure(StateMachineTransitionConfigurer transitions) throws Exception { - transitions - .withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY) - .and() - .withExternal().source(OrderStates.PAID).target(OrderStates.FULFILLED).event(OrderEvents.FULFILL) - .and() - .withExternal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELED).event(OrderEvents.CANCEL).guard(new Guard() { - @Override - public boolean evaluate(StateContext context) { - return false; - } - }) - .and() - .withExternal().source(OrderStates.PAID).target(OrderStates.CANCELED).event(OrderEvents.CANCEL); - -// -// .and() -// .withChoice() -// .source(OrderStates.SUBMITTED) -// .first(OrderStates.PAID2, new Guard() { -// @Override -// public boolean evaluate(StateContext context) { -// return false; -// } -// }) -// .last(OrderStates.PAID3) -// .and().withExternal().source(OrderStates.PAID2).target(OrderStates.CANCELED) -// .and().withExternal().source(OrderStates.PAID3).target(OrderStates.CANCELED); - } - - @Override - public void configure(StateMachineStateConfigurer states) throws Exception { - states.withStates() - .initial(OrderStates.SUBMITTED) - .state(OrderStates.PAID) -// .state(OrderStates.PAID2) -// .state(OrderStates.PAID3) - .end(OrderStates.FULFILLED) - .end(OrderStates.CANCELED); - - } - @Override - public void configure(StateMachineConfigurationConfigurer config) throws Exception { - StateMachineListenerAdapter listenerAdapter = new StateMachineListenerAdapter<>() { - @Override - public void stateChanged(State from, State to) { - log.info("state changed from {} to {}", from, to); - } - }; - config.withConfiguration() - .autoStartup(false) - .listener(listenerAdapter); - } } \ 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 new file mode 100644 index 0000000..2e7b413 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/TransitionParser.java @@ -0,0 +1,179 @@ +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 + '\'' + + '}'; + } +} + +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())); + + if (isChoice) { + transitions.addAll(parseChoiceTransitions(segment)); + } else { + transitions.add(parseStandardTransition(segment)); + } + } + + return transitions; + } + + private static List parseChoiceTransitions(List segment) { + List choiceTransitions = new ArrayList<>(); + String sourceState = null; + + 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 (name.equals("first") || name.equals("then") || name.equals("last")) { + if (args.isEmpty()) continue; + + Transition t = new Transition(); + t.type = "withChoice"; + t.sourceState = sourceState; + t.targetState = args.get(0).toString(); + t.event = null; + + if (args.size() > 1) { + Expression guardExpr = (Expression) args.get(1); + t.guard = guardExpr.toString(); + } + + choiceTransitions.add(t); + } + } + + return choiceTransitions; + } + private static final Set SUPPORTED_TRANSITION_TYPES = Set.of( + "withExternal", + "withInternal", + "withLocal", + "withFork", + "withJoin", + "withChoice" + ); + + 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; // Skip the rest of the switch for this iteration + } + + switch (name) { + case "source": + if (!args.isEmpty()) t.sourceState = args.get(0).toString(); + break; + case "target": + if (!args.isEmpty()) t.targetState = args.get(0).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(); + } + break; + } + } + + // Handle self-transition + if (t.sourceState != null && t.targetState == null) { + t.targetState = t.sourceState; + } + + return t; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/notgoodspringbased/SpringExporter.java b/src/main/java/com/example/statemachinedemo/notgoodspringbased/SpringExporter.java new file mode 100644 index 0000000..373fccb --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/notgoodspringbased/SpringExporter.java @@ -0,0 +1,222 @@ +package com.example.statemachinedemo.notgoodspringbased; + +import com.example.statemachinedemo.statemachine.OrderEvents; +import com.example.statemachinedemo.statemachine.OrderStates; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.state.PseudoStateKind; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; +import org.springframework.stereotype.Component; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.Collection; + +public class SpringExporter { +} + + +@Component +@Slf4j +@RequiredArgsConstructor +class Runner implements ApplicationRunner { + private final StateMachineFactory factory; + // +// @Override +// public void run(ApplicationArguments args) throws Exception { +// StateMachine stateMachine = factory.getStateMachine("order"); +// stateMachine.start(); +// log.info("Current state is {}", stateMachine.getState().getId()); +// +// } + private final Config1 c; + private final Config2 c2; + private final Config3 c3; + + @Override + public void run(ApplicationArguments args) throws Exception { + c.a(); + c2.a(); +// c3.exportToSCXML(); + } +} + + +class StateMachineDotExporter { + public static void export(StateMachine stateMachine, PrintWriter writer) { + writer.println("digraph stateMachine {"); + + for (State state : stateMachine.getStates()) { + writer.printf(" %s;%n", state.getId()); + } + + for (Transition transition : stateMachine.getTransitions()) { + if (transition.getSource() != null && transition.getTarget() != null) { + writer.printf(" %s -> %s [label=\"%s\"];%n", + transition.getSource().getId(), + transition.getTarget().getId(), + transition.getTrigger() != null ? transition.getTrigger().getEvent() : ""); + } + } + + writer.println("}"); + } +} + + +class StateMachinePlantUMLExporter { + + public static void export(StateMachine stateMachine, PrintWriter writer) { + writer.println("@startuml"); + + // Initial state + writer.println("[*] --> " + stateMachine.getInitialState().getId()); + + for (Transition transition : stateMachine.getTransitions()) { + if (transition.getSource() != null && transition.getTarget() != null) { + String source = transition.getSource().getId().toString(); + String target = transition.getTarget().getId().toString(); + + String label = ""; + + if (transition.getTrigger() != null && transition.getTrigger().getEvent() != null) { + label += transition.getTrigger().getEvent().toString(); + } + + if (transition.getGuard() != null) { + // If event is empty but there’s a guard, still label it meaningfully + if (!label.isEmpty()) { + label += " "; + } + label += "[guard]"; + } + + // If there's no label at all, don't include ":" + if (!label.isEmpty()) { + writer.printf("%s --> %s : %s%n", source, target, label); + } else { + writer.printf("%s --> %s%n", source, target); + } + } + } + + // End states + for (State state : stateMachine.getStates()) { + if (state.getPseudoState() != null && + state.getPseudoState().getKind() == PseudoStateKind.END) { + writer.printf("%s --> [*]%n", state.getId()); + } + } + + writer.println("@enduml"); + } +} + + +@Component +@RequiredArgsConstructor +class Config1 { + private final StateMachineFactory factory; + public void a() throws FileNotFoundException { + StateMachine stateMachine = factory.getStateMachine("order"); + + StateMachineDotExporter exporter = new StateMachineDotExporter(); + try (PrintWriter out = new PrintWriter("statemachine.dot")) { + StateMachineDotExporter.export(stateMachine, out); + } + } +} + +@Component +@RequiredArgsConstructor +class Config2 { + private final StateMachineFactory factory; + public void a() throws FileNotFoundException { + StateMachine stateMachine = factory.getStateMachine("order"); + + try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) { + StateMachinePlantUMLExporter.export(stateMachine, out); + } + } +} +@Component +@RequiredArgsConstructor +class Config3 { + private final StateMachineFactory factory; + public void exportToSCXML() throws Exception { + StateMachine stateMachine = factory.getStateMachine("order"); + + SCXMLExporter exporter = new SCXMLExporter(); + File file = new File("statemachine.scxml"); + exporter.exportToSCXML(stateMachine); + } +} + + +@Component +class SCXMLExporter { + + // This method assumes you already have a state machine + public void exportToSCXML(StateMachine stateMachine) throws Exception { + // Create a document to represent the SCXML structure + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.newDocument(); + + // Root SCXML element + Element scxmlElement = doc.createElement("scxml"); + scxmlElement.setAttribute("version", "1.0"); + doc.appendChild(scxmlElement); + + // Define the states + Element statesElement = doc.createElement("states"); + scxmlElement.appendChild(statesElement); + + // Iterate over the states of the state machine + Collection> states = stateMachine.getStates(); + for (State state : states) { + Element stateElement = doc.createElement("state"); + stateElement.setAttribute("id", state.getId().toString()); + statesElement.appendChild(stateElement); + } + + // Define the transitions + Element transitionsElement = doc.createElement("transitions"); + scxmlElement.appendChild(transitionsElement); + + Collection> transitions = stateMachine.getTransitions(); + for (Transition transition : transitions) { + Element transitionElement = doc.createElement("transition"); + transitionElement.setAttribute("from", transition.getSource().getId().toString()); + transitionElement.setAttribute("to", transition.getTarget().getId().toString()); + transitionElement.setAttribute("event", String.valueOf(transition.getTrigger())); + transitionsElement.appendChild(transitionElement); + } + + // Write to file + File file = new File("stateMachine.scxml"); + saveDocumentToFile(doc, file); + } + + private void saveDocumentToFile(Document doc, File file) throws Exception { + // Serialize the document to a file (you can use your preferred XML writer) + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + StreamResult result = new StreamResult(file); + DOMSource source = new DOMSource(doc); + transformer.transform(source, result); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/statemachinedemo/statemachine/OrderEvents.java b/src/main/java/com/example/statemachinedemo/statemachine/OrderEvents.java new file mode 100644 index 0000000..9cf826b --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/statemachine/OrderEvents.java @@ -0,0 +1,7 @@ +package com.example.statemachinedemo.statemachine; + +public enum OrderEvents { + FULFILL, PAY, CANCEL, + + ABCD, NOPE, IGNORE +} diff --git a/src/main/java/com/example/statemachinedemo/statemachine/OrderStates.java b/src/main/java/com/example/statemachinedemo/statemachine/OrderStates.java new file mode 100644 index 0000000..5d30ed3 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/statemachine/OrderStates.java @@ -0,0 +1,6 @@ +package com.example.statemachinedemo.statemachine; + +public enum OrderStates { + SUBMITTED, PAID, FULFILLED, CANCELED + ,PAID1,PAID2, PAID3, INVALID, HAPPEN, SKIPPED, NEVER +} \ 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 new file mode 100644 index 0000000..7cc6141 --- /dev/null +++ b/src/main/java/com/example/statemachinedemo/statemachine/SimpleEnumStateMachineConfiguration.java @@ -0,0 +1,96 @@ +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.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.guard.Guard; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; + +@Configuration +@EnableStateMachineFactory +@Slf4j +class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter { + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + Guard guard1 = null; + transitions + .withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY) + .and() + .withExternal().source(OrderStates.PAID).target(OrderStates.FULFILLED).event(OrderEvents.FULFILL) + .and() + .withExternal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELED).event(OrderEvents.CANCEL).guard(new Guard() { + @Override + public boolean evaluate(StateContext context) { + return false; + } + }) + .and() + .withExternal().source(OrderStates.PAID).target(OrderStates.CANCELED).event(OrderEvents.CANCEL) + + + .and() + .withExternal().source(OrderStates.SUBMITTED).event(OrderEvents.ABCD) + // it needs to have target specified to get added to .transitions collections +// .withExternal().source(OrderStates.SUBMITTED).target(OrderStates.SUBMITTED).event(OrderEvents.ABCD) +// + .and() + .withChoice() + .source(OrderStates.SUBMITTED) + .first(OrderStates.PAID2, new Guard() { + @Override + public boolean evaluate(StateContext context) { + return false; + } + }) + .last(OrderStates.PAID3) + .and() + .withChoice().source(OrderStates.PAID) + .first(OrderStates.PAID1, new Guard() { + @Override + public boolean evaluate(StateContext context) { + return true; + } + }) + .then(OrderStates.PAID2, guard1) + .then(OrderStates.HAPPEN, new Guard() { + @Override + public boolean evaluate(StateContext context) { + return false; + } + }) + .last(OrderStates.PAID3) + .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); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states.withStates() + .initial(OrderStates.SUBMITTED) + .state(OrderStates.PAID) + .state(OrderStates.PAID2) + .state(OrderStates.PAID3) + .end(OrderStates.FULFILLED) + .end(OrderStates.CANCELED); + } + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + StateMachineListenerAdapter listenerAdapter = new StateMachineListenerAdapter<>() { + @Override + public void stateChanged(State from, State to) { + log.info("state changed from {} to {}", from, to); + } + }; + config.withConfiguration() + .autoStartup(false) + .listener(listenerAdapter); + } +} \ No newline at end of file diff --git a/stateMachine.scxml b/stateMachine.scxml deleted file mode 100644 index b8022bd..0000000 --- a/stateMachine.scxml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/statemachine.1.dot b/statemachine.1.dot new file mode 100644 index 0000000..3a96d98 --- /dev/null +++ b/statemachine.1.dot @@ -0,0 +1,38 @@ +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]"]; + SUBMITTED_choice -> PAID3; + PAID_choice -> PAID1 [label=" [lambda]"]; + PAID_choice -> PAID2 [label=" [lambda]"]; + PAID_choice -> HAPPEN [label=" [lambda]"]; + PAID_choice -> PAID3; + PAID2 -> CANCELED; + PAID3 -> CANCELED; + PAID1 -> PAID1 [label="ABCD"]; +} + diff --git a/statemachine.2.dot b/statemachine.2.dot new file mode 100644 index 0000000..c0518c9 --- /dev/null +++ b/statemachine.2.dot @@ -0,0 +1,33 @@ +@startuml +skinparam state { + BackgroundColor<> LightYellow +} + +[*] --> SUBMITTED + +state PAID_choice <> +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 + +PAID1 --> [*] +CANCELED --> [*] +FULFILLED --> [*] +HAPPEN --> [*] +@enduml + diff --git a/statemachine.2.png b/statemachine.2.png new file mode 100644 index 0000000..0a9c97a Binary files /dev/null and b/statemachine.2.png differ diff --git a/statemachine.dot b/statemachine.dot deleted file mode 100644 index ccfa158..0000000 --- a/statemachine.dot +++ /dev/null @@ -1,14 +0,0 @@ -digraph stateMachine { - PAID2; - SUBMITTED; - PAID; - PAID3; - CANCELED; - FULFILLED; - 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 dc48df7..910b84e 100644 Binary files a/statemachine.png and b/statemachine.png differ diff --git a/statemachine.uml.dot b/statemachine.uml.dot deleted file mode 100644 index ecd41d4..0000000 --- a/statemachine.uml.dot +++ /dev/null @@ -1,11 +0,0 @@ -@startuml -[*] --> SUBMITTED -SUBMITTED --> PAID : PAY -PAID --> FULFILLED : FULFILL -SUBMITTED --> CANCELED : CANCEL -PAID --> CANCELED : CANCEL -PAID2 --> CANCELED -PAID3 --> CANCELED -CANCELED --> [*] -FULFILLED --> [*] -@enduml diff --git a/statemachine.uml.png b/statemachine.uml.png deleted file mode 100644 index 574191c..0000000 Binary files a/statemachine.uml.png and /dev/null differ