This commit is contained in:
2025-07-05 21:32:36 +02:00
parent 3ad99735ba
commit c59947a0ac
18 changed files with 1294 additions and 289 deletions

View File

@@ -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'

View File

@@ -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<Path> javaFiles = findJavaFiles(Paths.get(START_DIR));
for (Path javaFile : javaFiles) {
String content = Files.readString(javaFile);
boolean findAnnotations = false;
if (findAnnotations) {
if (hasClassWithAnnotation(content, TARGET_ANNOTATION)) {
System.out.println("Found annotation in: " + javaFile.toAbsolutePath());
}
}
boolean printBody = false;
if (printBody) {
if (hasClassWithSuperAndConfigureMethodExactSignature(
content,
TARGET_SUPERCLASS,
TARGET_METHOD_NAME,
TARGET_METHOD_PARAM,
TARGET_THROWS_EXCEPTION)) {
System.out.println("Found class with exact configure signature in: " + javaFile.toAbsolutePath());
}
String methodBody = getConfigureMethodBodyExactSignature(
content,
TARGET_SUPERCLASS,
TARGET_METHOD_NAME,
TARGET_METHOD_PARAM,
TARGET_THROWS_EXCEPTION);
if (methodBody != null) {
System.out.println("Method body:\n" + methodBody);
}
}
String source = Files.readString(javaFile.toAbsolutePath());
MethodDeclaration configureMethod = ConfigureMethodFinder.findConfigureMethod(source);
if (configureMethod != null) {
System.out.println("Found configure method!");
// Now pass configureMethod to your TransitionParser.parseTransitions(configureMethod)
List<Transition> transitions = TransitionParser.parseTransitions(configureMethod);
for (Transition t : transitions) {
System.out.println(t);
}
// Find start and end states
Set<String> startStates = StateUtils.findStartStates(transitions);
Set<String> endStates = StateUtils.findEndStates(transitions);
System.out.println("Start States: " + startStates);
System.out.println("End States: " + endStates);
String s = generatePlantUML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.2.dot")) {
out.println(s);
}
String s1 = generateDot(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.1.dot")) {
out.println(s1);
}
String s2 = exportToSCXML(transitions, startStates, endStates, true);
try (PrintWriter out = new PrintWriter("statemachine.3.xml")) {
out.println(s2);
}
}
// else {
// System.out.println("No configure method matching criteria found.");
// }
}
}
private static List<Path> findJavaFiles(Path startDir) throws IOException {
try (Stream<Path> paths = Files.walk(startDir)) {
return paths
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(EXTENSION))
.collect(Collectors.toList());
}
}
private static boolean hasClassWithAnnotation(String source, String annotationName) {
ASTParser parser = ASTParser.newParser(AST.JLS17);
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
final boolean[] found = {false};
cu.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
if (node.isInterface()) return true;
for (Object modifier : node.modifiers()) {
if (modifier instanceof Annotation) {
Annotation annotation = (Annotation) modifier;
String name = annotation.getTypeName().getFullyQualifiedName();
if (name.equals(annotationName)) {
found[0] = true;
return false;
}
}
}
return true;
}
});
return found[0];
}
private static String getConfigureMethodBodyExactSignature(
String source,
String targetSuperclassName,
String methodName,
String methodParamType,
String throwsExceptionType) {
ASTParser parser = ASTParser.newParser(AST.JLS17);
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
final String[] bodyString = {null};
cu.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
if (node.isInterface()) return true;
Type superType = node.getSuperclassType();
if (superType != null) {
String superName = superType.toString();
if (superName.startsWith(targetSuperclassName)) {
MethodDeclaration[] methods = node.getMethods();
for (MethodDeclaration method : methods) {
if (!method.getName().getIdentifier().equals(methodName)) {
continue;
}
Type returnType = method.getReturnType2();
if (returnType == null || !returnType.isPrimitiveType() ||
((PrimitiveType) returnType).getPrimitiveTypeCode() != PrimitiveType.VOID) {
continue;
}
boolean hasOverride = false;
for (Object mod : method.modifiers()) {
if (mod instanceof Annotation) {
Annotation annotation = (Annotation) mod;
if (annotation.getTypeName().getFullyQualifiedName().equals("Override")) {
hasOverride = true;
break;
}
}
}
if (!hasOverride) continue;
List<?> parameters = method.parameters();
if (parameters.size() != 1) continue;
SingleVariableDeclaration param = (SingleVariableDeclaration) parameters.get(0);
String paramTypeName = param.getType().toString();
if (!paramTypeName.startsWith(methodParamType)) continue;
@SuppressWarnings("unchecked")
List<Type> thrownExceptions = method.thrownExceptionTypes();
boolean throwsExceptionFound = false;
for (Type excType : thrownExceptions) {
if (excType.toString().equals(throwsExceptionType)) {
throwsExceptionFound = true;
break;
}
}
if (!throwsExceptionFound) continue;
Block body = method.getBody();
if (body != null) {
bodyString[0] = body.toString();
}
return false;
}
}
}
return true;
}
});
return bodyString[0];
}
private static boolean hasClassWithSuperAndConfigureMethodExactSignature(
String source,
String targetSuperclassName,
String methodName,
String methodParamType,
String throwsExceptionType) {
ASTParser parser = ASTParser.newParser(AST.JLS17);
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
final boolean[] found = {false};
cu.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
if (node.isInterface()) return true;
Type superType = node.getSuperclassType();
if (superType != null) {
String superName = superType.toString();
if (superName.startsWith(targetSuperclassName)) {
MethodDeclaration[] methods = node.getMethods();
for (MethodDeclaration method : methods) {
// 1) Check method name
if (!method.getName().getIdentifier().equals(methodName)) {
continue;
}
// 2) Check return type is void
Type returnType = method.getReturnType2();
if (returnType == null || !returnType.isPrimitiveType() ||
((PrimitiveType) returnType).getPrimitiveTypeCode() != PrimitiveType.VOID) {
continue;
}
// 3) Check @Override annotation present
boolean hasOverride = false;
for (Object mod : method.modifiers()) {
if (mod instanceof Annotation) {
Annotation annotation = (Annotation) mod;
if (annotation.getTypeName().getFullyQualifiedName().equals("Override")) {
hasOverride = true;
break;
}
}
}
if (!hasOverride) continue;
// 4) Check parameters - exactly 1 parameter of the right type
List<?> parameters = method.parameters();
if (parameters.size() != 1) continue;
SingleVariableDeclaration param = (SingleVariableDeclaration) parameters.get(0);
String paramTypeName = param.getType().toString();
if (!paramTypeName.startsWith(methodParamType)) continue;
// 5) Check throws clause includes Exception
@SuppressWarnings("unchecked")
List<Type> thrownExceptions = method.thrownExceptionTypes();
boolean throwsExceptionFound = false;
for (Type excType : thrownExceptions) {
String excName = excType.toString();
if (excName.equals(throwsExceptionType)) {
throwsExceptionFound = true;
break;
}
}
if (!throwsExceptionFound) continue;
// All conditions met!
found[0] = true;
return false;
}
}
}
return true;
}
});
return found[0];
}
}
class ConfigureMethodFinder {
/**
* Parses source code and returns the MethodDeclaration for
* `public void configure(StateMachineTransitionConfigurer<T,D> transitions) throws Exception`
* in a class extending StateMachineConfigurerAdapter.
*
* @param source Java source code string
* @return MethodDeclaration of configure or null if not found
*/
public static MethodDeclaration findConfigureMethod(String source) {
ASTParser parser = ASTParser.newParser(AST.JLS17);
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
ConfigureMethodVisitor visitor = new ConfigureMethodVisitor();
cu.accept(visitor);
return visitor.getConfigureMethod();
}
private static class ConfigureMethodVisitor extends ASTVisitor {
private MethodDeclaration configureMethod = null;
@Override
public boolean visit(TypeDeclaration node) {
// Check if class extends StateMachineConfigurerAdapter
if (!node.isInterface() && extendsStateMachineConfigurerAdapter(node)) {
// Look for configure method inside this class
for (MethodDeclaration method : node.getMethods()) {
if (isConfigureMethod(method)) {
configureMethod = method;
return false; // Found, stop visiting further
}
}
}
return true;
}
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration node) {
Type superclass = node.getSuperclassType();
if (superclass == null) return false;
// Superclass can be parameterized type
if (superclass.isParameterizedType()) {
ParameterizedType pt = (ParameterizedType) superclass;
String superName = pt.getType().toString();
return superName.equals("StateMachineConfigurerAdapter");
} else {
return superclass.toString().equals("StateMachineConfigurerAdapter");
}
}
private boolean isConfigureMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure")) return false;
// Must be public void
if (!Modifier.isPublic(method.getModifiers())) return false;
if (!(method.getReturnType2() != null && method.getReturnType2().isPrimitiveType())) return false;
if (!"void".equals(method.getReturnType2().toString())) return false;
// Exactly one parameter of type StateMachineTransitionConfigurer<T,D>
List<?> params = method.parameters();
if (params.size() != 1) return false;
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(0);
Type paramType = param.getType();
String paramTypeStr = paramType.toString();
// Since generic params, just check starts with StateMachineTransitionConfigurer
if (!paramTypeStr.startsWith("StateMachineTransitionConfigurer")) return false;
// Check throws Exception using thrownExceptionTypes()
@SuppressWarnings("unchecked")
List<Type> thrownExceptions = method.thrownExceptionTypes();
boolean throwsExceptionFound = false;
for (Type excType : thrownExceptions) {
if (excType.toString().equals("Exception")) {
throwsExceptionFound = true;
break;
}
}
if (!throwsExceptionFound) return false;
return true;
}
public MethodDeclaration getConfigureMethod() {
return configureMethod;
}
}
}

View File

@@ -0,0 +1,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<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards) {
StringBuilder sb = new StringBuilder();
sb.append("@startuml\n");
sb.append("skinparam state {\n");
sb.append(" BackgroundColor<<choice>> LightYellow\n");
sb.append("}\n\n");
// 1. Print start states
for (String start : startStates) {
sb.append("[*] --> ").append(simplify(start)).append("\n");
}
sb.append("\n");
// 2. Find all states that have choice transitions originating from them
// We'll reroute those transitions through a separate choice pseudostate.
Set<String> 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(" <<choice>>\n");
sb.append(state).append(" --> ").append(choiceState).append("\n");
}
sb.append("\n");
Set<String> 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<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards) {
StringBuilder sb = new StringBuilder();
sb.append("digraph stateMachine {\n");
sb.append(" rankdir=LR;\n");
sb.append(" node [shape=circle, style=filled, fillcolor=lightgray];\n\n");
// 1. Fake start node
sb.append(" start [shape=point, label=\"\"];\n");
// 2. Find states with choice transitions
Set<String> statesWithChoice = new HashSet<>();
for (Transition t : transitions) {
if ("withChoice".equals(t.type)) {
statesWithChoice.add(simplify(t.sourceState));
}
}
// 3. Declare normal states and choice pseudostates
Set<String> 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<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
sb.append("<scxml xmlns=\"http://www.w3.org/2005/07/scxml\" version=\"1.0\">\n\n");
// 1. Collect all states
Set<String> allStates = new HashSet<>();
for (Transition t : transitions) {
if (t.sourceState != null) allStates.add(t.sourceState);
if (t.targetState != null) allStates.add(t.targetState);
}
// 2. Output <state> elements with transitions
for (String state : allStates) {
String simpleState = simplify(state);
sb.append(" <state id=\"").append(simpleState).append("\">\n");
// If this state is a start state, add <initial> and transition to it
if (startStates.contains(state)) {
sb.append(" <initial>\n");
sb.append(" <transition target=\"").append(simpleState).append("\"/>\n");
sb.append(" </initial>\n");
}
// Add transitions from this state
for (Transition t : transitions) {
if (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(" <transition");
if (event != null && !event.isBlank()) sb.append(" event=\"").append(event).append("\"");
sb.append(" target=\"").append(target).append("\"");
if (guard != null && !guard.isBlank()) sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
sb.append("/>\n");
}
}
sb.append(" </state>\n\n");
}
// 3. Add final states (if not already defined as states)
for (String end : endStates) {
String simpleEnd = simplify(end);
if (!allStates.contains(end)) {
sb.append(" <final id=\"").append(simpleEnd).append("\"/>\n\n");
}
}
sb.append("</scxml>\n");
return sb.toString();
}
private static String escapeXml(String s) {
if (s == null) return "";
return s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
// Helper: extract last enum part
private static String simplify(String full) {
if (full == null) return "";
int dot = full.lastIndexOf('.');
return dot >= 0 ? full.substring(dot + 1) : full;
}
}

View File

@@ -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<String> findStartStates(List<Transition> transitions) {
Set<String> sources = new HashSet<>();
Set<String> 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<String> findEndStates(List<Transition> transitions) {
Set<String> targets = new HashSet<>();
Set<String> 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("^\"|\"$", "");
}
}

View File

@@ -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<OrderStates, OrderEvents> factory;
//
// @Override
// public void run(ApplicationArguments args) throws Exception {
// StateMachine<OrderStates, OrderEvents> 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 <S, E> void export(StateMachine<S, E> stateMachine, PrintWriter writer) {
writer.println("digraph stateMachine {");
for (State<S, E> state : stateMachine.getStates()) {
writer.printf(" %s;%n", state.getId());
}
for (Transition<S, E> 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 <S, E> void export(StateMachine<S, E> stateMachine, PrintWriter writer) {
writer.println("@startuml");
writer.println("[*] --> " + stateMachine.getInitialState().getId());
for (Transition<S, E> 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<S, E> 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<OrderStates, OrderEvents> factory;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> 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<OrderStates, OrderEvents> factory;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> 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 <S, E> void exportToSCXML(StateMachine<S, E> 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<State<S, E>> states = stateMachine.getStates();
for (State<S, E> 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<Transition<S, E>> transitions = stateMachine.getTransitions();
for (Transition<S, E> 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<OrderStates, OrderEvents> factory;
public void exportToSCXML() throws Exception {
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
SCXMLExporter exporter = new SCXMLExporter();
File file = new File("statemachine.scxml");
exporter.exportToSCXML(stateMachine);
}
}
@Configuration
@EnableStateMachineFactory
@Slf4j
class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> 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<OrderStates, OrderEvents>() {
@Override
public boolean evaluate(StateContext<OrderStates, OrderEvents> context) {
return false;
}
})
.and()
.withExternal().source(OrderStates.PAID).target(OrderStates.CANCELED).event(OrderEvents.CANCEL);
//
// .and()
// .withChoice()
// .source(OrderStates.SUBMITTED)
// .first(OrderStates.PAID2, new Guard<OrderStates, OrderEvents>() {
// @Override
// public boolean evaluate(StateContext<OrderStates, OrderEvents> 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<OrderStates, OrderEvents> 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<OrderStates, OrderEvents> config) throws Exception {
StateMachineListenerAdapter<OrderStates, OrderEvents> listenerAdapter = new StateMachineListenerAdapter<>() {
@Override
public void stateChanged(State<OrderStates, OrderEvents> from, State<OrderStates, OrderEvents> to) {
log.info("state changed from {} to {}", from, to);
}
};
config.withConfiguration()
.autoStartup(false)
.listener(listenerAdapter);
}
}

View File

@@ -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<Transition> parseTransitions(MethodDeclaration method) {
List<Transition> transitions = new ArrayList<>();
Block body = method.getBody();
if (body == null) return transitions;
for (Object stmtObj : body.statements()) {
if (!(stmtObj instanceof ExpressionStatement)) continue;
ExpressionStatement stmt = (ExpressionStatement) stmtObj;
Expression expr = stmt.getExpression();
if (!(expr instanceof MethodInvocation)) continue;
transitions.addAll(parseTransitionsFromExpression((MethodInvocation) expr));
}
return transitions;
}
private static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall) {
List<Transition> transitions = new ArrayList<>();
// Step 1: Unroll method chain into ordered list
List<MethodInvocation> calls = new ArrayList<>();
Expression current = rootCall;
while (current instanceof MethodInvocation) {
MethodInvocation mi = (MethodInvocation) current;
calls.add(0, mi);
current = mi.getExpression();
}
// Step 2: Split into segments by .and()
List<List<MethodInvocation>> segments = new ArrayList<>();
List<MethodInvocation> currentSegment = new ArrayList<>();
for (MethodInvocation call : calls) {
if ("and".equals(call.getName().getIdentifier())) {
if (!currentSegment.isEmpty()) {
segments.add(currentSegment);
currentSegment = new ArrayList<>();
}
} else {
currentSegment.add(call);
}
}
if (!currentSegment.isEmpty()) {
segments.add(currentSegment);
}
// Step 3: Parse each segment
for (List<MethodInvocation> segment : segments) {
boolean isChoice = segment.stream()
.anyMatch(mi -> "withChoice".equals(mi.getName().getIdentifier()));
if (isChoice) {
transitions.addAll(parseChoiceTransitions(segment));
} else {
transitions.add(parseStandardTransition(segment));
}
}
return transitions;
}
private static List<Transition> parseChoiceTransitions(List<MethodInvocation> segment) {
List<Transition> 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<String> SUPPORTED_TRANSITION_TYPES = Set.of(
"withExternal",
"withInternal",
"withLocal",
"withFork",
"withJoin",
"withChoice"
);
private static Transition parseStandardTransition(List<MethodInvocation> segment) {
Transition t = new Transition();
for (MethodInvocation call : segment) {
String name = call.getName().getIdentifier();
List<?> args = call.arguments();
if (SUPPORTED_TRANSITION_TYPES.contains(name)) {
t.type = name;
continue; // 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;
}
}

View File

@@ -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<OrderStates, OrderEvents> factory;
//
// @Override
// public void run(ApplicationArguments args) throws Exception {
// StateMachine<OrderStates, OrderEvents> 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 <S, E> void export(StateMachine<S, E> stateMachine, PrintWriter writer) {
writer.println("digraph stateMachine {");
for (State<S, E> state : stateMachine.getStates()) {
writer.printf(" %s;%n", state.getId());
}
for (Transition<S, E> 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 <S, E> void export(StateMachine<S, E> stateMachine, PrintWriter writer) {
writer.println("@startuml");
// Initial state
writer.println("[*] --> " + stateMachine.getInitialState().getId());
for (Transition<S, E> 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 theres 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<S, E> 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<OrderStates, OrderEvents> factory;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> 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<OrderStates, OrderEvents> factory;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) {
StateMachinePlantUMLExporter.export(stateMachine, out);
}
}
}
@Component
@RequiredArgsConstructor
class Config3 {
private final StateMachineFactory<OrderStates, OrderEvents> factory;
public void exportToSCXML() throws Exception {
StateMachine<OrderStates, OrderEvents> 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 <S, E> void exportToSCXML(StateMachine<S, E> 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<State<S, E>> states = stateMachine.getStates();
for (State<S, E> 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<org.springframework.statemachine.transition.Transition<S, E>> transitions = stateMachine.getTransitions();
for (Transition<S, E> 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);
}
}

View File

@@ -0,0 +1,7 @@
package com.example.statemachinedemo.statemachine;
public enum OrderEvents {
FULFILL, PAY, CANCEL,
ABCD, NOPE, IGNORE
}

View File

@@ -0,0 +1,6 @@
package com.example.statemachinedemo.statemachine;
public enum OrderStates {
SUBMITTED, PAID, FULFILLED, CANCELED
,PAID1,PAID2, PAID3, INVALID, HAPPEN, SKIPPED, NEVER
}

View File

@@ -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<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
Guard<OrderStates, OrderEvents> 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<OrderStates, OrderEvents>() {
@Override
public boolean evaluate(StateContext<OrderStates, OrderEvents> 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<OrderStates, OrderEvents>() {
@Override
public boolean evaluate(StateContext<OrderStates, OrderEvents> context) {
return false;
}
})
.last(OrderStates.PAID3)
.and()
.withChoice().source(OrderStates.PAID)
.first(OrderStates.PAID1, new Guard<OrderStates, OrderEvents>() {
@Override
public boolean evaluate(StateContext<OrderStates, OrderEvents> context) {
return true;
}
})
.then(OrderStates.PAID2, guard1)
.then(OrderStates.HAPPEN, new Guard<OrderStates, OrderEvents>() {
@Override
public boolean evaluate(StateContext<OrderStates, OrderEvents> 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<OrderStates, OrderEvents> 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<OrderStates, OrderEvents> config) throws Exception {
StateMachineListenerAdapter<OrderStates, OrderEvents> listenerAdapter = new StateMachineListenerAdapter<>() {
@Override
public void stateChanged(State<OrderStates, OrderEvents> from, State<OrderStates, OrderEvents> to) {
log.info("state changed from {} to {}", from, to);
}
};
config.withConfiguration()
.autoStartup(false)
.listener(listenerAdapter);
}
}

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><scxml version="1.0"><states><state id="CANCELED"/><state id="PAID"/><state id="FULFILLED"/><state id="SUBMITTED"/></states><transitions><transition event="org.springframework.statemachine.trigger.EventTrigger@3ba348ca" from="SUBMITTED" to="PAID"/><transition event="org.springframework.statemachine.trigger.EventTrigger@56e9a474" from="PAID" to="FULFILLED"/><transition event="org.springframework.statemachine.trigger.EventTrigger@45b15381" from="SUBMITTED" to="CANCELED"/><transition event="org.springframework.statemachine.trigger.EventTrigger@2dbfcf7" from="PAID" to="CANCELED"/></transitions></scxml>

38
statemachine.1.dot Normal file
View File

@@ -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"];
}

33
statemachine.2.dot Normal file
View File

@@ -0,0 +1,33 @@
@startuml
skinparam state {
BackgroundColor<<choice>> LightYellow
}
[*] --> SUBMITTED
state PAID_choice <<choice>>
PAID --> PAID_choice
state SUBMITTED_choice <<choice>>
SUBMITTED --> SUBMITTED_choice
SUBMITTED --> PAID : PAY
PAID --> FULFILLED : FULFILL
SUBMITTED --> CANCELED : CANCEL [lambda]
PAID --> CANCELED : CANCEL
SUBMITTED --> SUBMITTED : ABCD
SUBMITTED_choice --> PAID2 : [lambda]
SUBMITTED_choice --> PAID3
PAID_choice --> PAID1 : [lambda]
PAID_choice --> PAID2 : [lambda]
PAID_choice --> HAPPEN : [lambda]
PAID_choice --> PAID3
PAID2 --> CANCELED
PAID3 --> CANCELED
PAID1 --> PAID1 : ABCD
PAID1 --> [*]
CANCELED --> [*]
FULFILLED --> [*]
HAPPEN --> [*]
@enduml

BIN
statemachine.2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -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=""];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB