package change

This commit is contained in:
2025-07-13 20:11:41 +02:00
parent 4fc75a534e
commit 0cc9af9a08
23 changed files with 248 additions and 289 deletions

View File

@@ -0,0 +1,11 @@
package click.kamil.springstatemachineexporter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StatemachinedemoApplication {
public static void main(String[] args) {
SpringApplication.run(StatemachinedemoApplication.class, args);
}
}

View File

@@ -0,0 +1,90 @@
package click.kamil.springstatemachineexporter.ast;
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
import click.kamil.springstatemachineexporter.ast.app.StateMachineStateConfigurationMethodFinder;
import click.kamil.springstatemachineexporter.ast.app.StateMachineTransitionConfigurationMethodFinder;
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.common.FileUtils;
import click.kamil.springstatemachineexporter.ast.in.AstFileFinder;
import click.kamil.springstatemachineexporter.ast.out.Dot;
import click.kamil.springstatemachineexporter.ast.out.PlantUml;
import click.kamil.springstatemachineexporter.ast.out.Scxml;
import click.kamil.springstatemachineexporter.ast.out.StateMachineExporter;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Main {
private static final String START_DIR = ".";
private static final String EXTENSION = ".java";
private static final String TARGET_ANNOTATION = "EnableStateMachineFactory";
public static void main(String[] args) throws IOException {
List<Path> javaFiles = FileUtils.findFilesWithExtension(Paths.get(START_DIR), EXTENSION);
// List of output handlers
List<StateMachineExporter> outputs = List.of(
new PlantUml(),
new Dot(),
new Scxml()
);
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
AstFileFinder finder = new AstFileFinder(source);
if (finder.hasClassWithAnnotation(source, TARGET_ANNOTATION)) {
System.out.println("Found annotation in: " + javaFile.toAbsolutePath());
}
MethodDeclaration configureMethod = new StateMachineTransitionConfigurationMethodFinder(source).findConfigureMethod();
if (configureMethod != null) {
System.out.println("Found configure method in: " + javaFile.toAbsolutePath());
System.out.println("Method body:\n" + configureMethod.getBody());
List<Transition> transitions = AstTransitionParser.parseTransitions(configureMethod);
for (Transition t : transitions) {
System.out.println(t);
}
Set<String> startStatesTransitions = TransitionStateUtils.findStartStates(transitions);
Set<String> endStatesTransitions = TransitionStateUtils.findEndStates(transitions);
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor = new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
System.out.println("Start States Ast: " + visitor.getInitialStates());
System.out.println("End States Ast: " + visitor.getEndStates());
System.out.println("Start States: " + startStatesTransitions);
System.out.println("End States: " + endStatesTransitions);
Set<String> startStates = new HashSet<>(startStatesTransitions);
startStates.addAll(visitor.getInitialStates());
Set<String> endStates = new HashSet<>(endStatesTransitions);
endStates.addAll(visitor.getEndStates());
String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", "");
// Iterate over outputs and generate files dynamically
for (StateMachineExporter output : outputs) {
String content = output.export(transitions, startStates, endStates, true);
String fileName = baseName + output.getFileExtension();
try (PrintWriter out = new PrintWriter(fileName)) {
out.println(content);
}
}
}
}
}
}

View File

@@ -0,0 +1,157 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public class AstTransitionParser {
private static final Set<String> SUPPORTED_TRANSITION_TYPES = Set.of(
"withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction"
);
public static List<Transition> parseTransitions(MethodDeclaration method) {
List<Transition> transitions = new ArrayList<>();
Optional.ofNullable(method)
.map(MethodDeclaration::getBody)
.ifPresent(body -> {
for (Object stmt : body.statements()) {
if (stmt instanceof ExpressionStatement es
&& es.getExpression() instanceof MethodInvocation mi) {
transitions.addAll(parseTransitionsFromExpression(mi));
}
}
});
return transitions;
}
private static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall) {
List<Transition> transitions = new ArrayList<>();
// Step 1: Unroll method chain
List<MethodInvocation> calls = new ArrayList<>();
Expression current = rootCall;
while (current instanceof MethodInvocation mi) {
calls.addFirst(mi);
current = mi.getExpression();
}
// Step 2: Split 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()));
boolean isJunction = segment.stream().anyMatch(mi -> "withJunction".equals(mi.getName().getIdentifier()));
if (isChoice || isJunction) {
transitions.addAll(parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction"));
} else {
transitions.add(parseStandardTransition(segment));
}
}
return transitions;
}
private static List<Transition> parseChoiceOrJunctionTransitions(List<MethodInvocation> segment, String type) {
List<Transition> transitions = new ArrayList<>();
String sourceState = null;
int orderCounter = 0;
// Extract source state once
for (MethodInvocation call : segment) {
if ("source".equals(call.getName().getIdentifier()) && !call.arguments().isEmpty()) {
sourceState = call.arguments().getFirst().toString();
break; // only take the first occurrence
}
}
for (MethodInvocation call : segment) {
String methodName = call.getName().getIdentifier();
List<?> args = call.arguments();
if (!List.of("first", "then", "last").contains(methodName) || args.isEmpty()) {
continue;
}
Transition t = new Transition();
t.setType(type);
if (sourceState != null) {
t.getSourceStates().add(sourceState);
}
t.getTargetStates().add(args.get(0).toString());
if (args.size() > 1) {
parseGuard(args.get(1), t);
}
if ("then".equals(methodName) && args.size() > 2) {
parseAction(args.get(2), t);
}
t.setOrder(orderCounter++);
transitions.add(t);
}
return transitions;
}
private static Transition parseStandardTransition(List<MethodInvocation> segment) {
Transition t = new Transition();
for (MethodInvocation call : segment) {
String methodName = call.getName().getIdentifier();
List<?> args = call.arguments();
if (SUPPORTED_TRANSITION_TYPES.contains(methodName)) {
t.setType(methodName);
continue;
}
if (args.isEmpty()) {
continue;
}
switch (methodName) {
case "source" -> args.forEach(arg -> t.getSourceStates().add(arg.toString()));
case "target" -> args.forEach(arg -> t.getTargetStates().add(arg.toString()));
case "event" -> t.setEvent(args.getFirst().toString());
case "guard" -> parseGuard(args.getFirst(), t);
case "action" -> parseAction(args.getFirst(), t);
}
}
return t;
}
private static void parseGuard(Object arg, Transition t) {
if (arg instanceof Expression expr) {
t.setGuard(expr.toString());
t.setLambdaGuard(isLambdaOrAnonymous(expr));
}
}
private static void parseAction(Object arg, Transition t) {
if (arg instanceof Expression expr) {
t.getActions().add(expr.toString());
t.getIsLambdaActions().add(isLambdaOrAnonymous(expr));
}
}
public static boolean isLambdaOrAnonymous(Expression expr) {
return switch (expr) {
case null -> false;
case LambdaExpression le -> true;
case ClassInstanceCreation cic -> cic.getAnonymousClassDeclaration() != null;
default -> false;
};
}
}

View File

@@ -0,0 +1,106 @@
package click.kamil.springstatemachineexporter.ast.app;
import lombok.Getter;
import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class StateMachineStateConfigurationMethodFinder {
public static CompilationUnit parse(String source) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
return (CompilationUnit) parser.createAST(null);
}
@Getter
public static class StateConfigurerVisitor extends ASTVisitor {
private final Set<String> initialStates = new HashSet<>();
private final Set<String> endStates = new HashSet<>();
@Override
public boolean visit(MethodDeclaration node) {
// Visit method named "configure" only
if (!"configure".equals(node.getName().getIdentifier())) {
return super.visit(node);
}
Block body = node.getBody();
if (body == null) return false;
// Traverse statements inside configure method
for (Object stmtObj : body.statements()) {
if (!(stmtObj instanceof ExpressionStatement exprStmt)) continue;
Expression expr = exprStmt.getExpression();
if (expr instanceof MethodInvocation rootCall) {
// Check if this chain starts with "withStates"
if (isWithStatesChain(rootCall)) {
parseMethodChain(rootCall);
}
}
}
return false; // no need to visit further inside configure
}
private boolean isWithStatesChain(MethodInvocation mi) {
MethodInvocation current = mi;
while (true) {
Expression expr = current.getExpression();
if ("withStates".equals(current.getName().getIdentifier())) {
return true;
}
if (expr instanceof MethodInvocation next) {
current = next;
} else {
return false;
}
}
}
private boolean isInsideRegionOrFork(MethodInvocation mi) {
Expression expr = mi.getExpression();
while (expr instanceof MethodInvocation parent) {
String methodName = parent.getName().getIdentifier();
if (List.of("fork", "region").contains(methodName)) {
return true;
}
expr = parent.getExpression();
}
return false;
}
private void parseMethodChain(MethodInvocation mi) {
List<MethodInvocation> chain = new ArrayList<>();
Expression current = mi;
while (current instanceof MethodInvocation m) {
chain.addFirst(m);
current = m.getExpression();
}
for (MethodInvocation call : chain) {
String methodName = call.getName().getIdentifier();
List<?> args = call.arguments();
if (args.isEmpty()) {
continue;
}
Expression arg = (Expression) args.getFirst();
switch (methodName) {
case "initial" -> {
if (!isInsideRegionOrFork(call)) {
initialStates.add(arg.toString());
}
}
case "end" -> endStates.add(arg.toString());
// ignore others
}
}
}
}
}

View File

@@ -0,0 +1,81 @@
package click.kamil.springstatemachineexporter.ast.app;
import lombok.Getter;
import org.eclipse.jdt.core.dom.*;
import java.util.List;
public class StateMachineTransitionConfigurationMethodFinder {
private final CompilationUnit cu;
private final ConfigureMethodVisitor visitor = new ConfigureMethodVisitor();
public StateMachineTransitionConfigurationMethodFinder(String source) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
this.cu = (CompilationUnit) parser.createAST(null);
}
public MethodDeclaration findConfigureMethod() {
this.cu.accept(visitor);
return visitor.getConfigureMethod();
}
@Getter
private static class ConfigureMethodVisitor extends ASTVisitor {
private MethodDeclaration configureMethod = null;
@Override
public boolean visit(TypeDeclaration node) {
if (!node.isInterface() && extendsStateMachineConfigurerAdapter(node)) {
for (MethodDeclaration method : node.getMethods()) {
if (isConfigureMethod(method)) {
configureMethod = method;
return false; // Found, stop visiting further
}
}
}
return true;
}
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration node) {
Type superclass = node.getSuperclassType();
if (superclass == null)
return false;
if (superclass.isParameterizedType()) {
ParameterizedType pt = (ParameterizedType) superclass;
return pt.getType().toString().equals("StateMachineConfigurerAdapter");
}
return superclass.toString().equals("StateMachineConfigurerAdapter");
}
private boolean isConfigureMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure")) return false;
if (!Modifier.isPublic(method.getModifiers())) return false;
Type returnType = method.getReturnType2();
if (returnType == null || !returnType.isPrimitiveType() || !"void".equals(returnType.toString()))
return false;
@SuppressWarnings("unchecked")
List<SingleVariableDeclaration> params = method.parameters();
if (params.size() != 1)
return false;
if (!params.getFirst().getType().toString().startsWith("StateMachineTransitionConfigurer"))
return false;
@SuppressWarnings("unchecked")
List<Type> thrownExceptions = method.thrownExceptionTypes();
for (Type excType : thrownExceptions) {
if (excType.toString().equals("Exception")) {
return true;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,42 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TransitionStateUtils {
public static Set<String> findStartStates(Collection<Transition> transitions) {
Set<String> allTargetStates = transitions.stream()
.flatMap(t -> normalizeStates(t.getTargetStates()))
.collect(Collectors.toSet());
return transitions.stream()
.flatMap(t -> normalizeStates(t.getSourceStates()))
.filter(state -> !allTargetStates.contains(state))
.collect(Collectors.toSet());
}
public static Set<String> findEndStates(Collection<Transition> transitions) {
Set<String> allSourceStates = transitions.stream()
.flatMap(t -> normalizeStates(t.getSourceStates()))
.collect(Collectors.toSet());
return transitions.stream()
.flatMap(t -> normalizeStates(t.getTargetStates()))
.filter(state -> !allSourceStates.contains(state))
.collect(Collectors.toSet());
}
private static Stream<String> normalizeStates(Collection<String> states) {
return states.stream()
.map(TransitionStateUtils::stripQuotes)
.filter(Objects::nonNull);
}
private static String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
}

View File

@@ -0,0 +1,26 @@
package click.kamil.springstatemachineexporter.ast.app.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
@ToString
@Getter
@Setter
public class Transition {
private String type; // withExternal, withChoice, withInternal, etc.
private List<String> sourceStates = new ArrayList<>();
private List<String> targetStates = new ArrayList<>();
private String event;
private String guard;
private boolean isLambdaGuard = false;
private List<String> actions = new ArrayList<>();
private List<Boolean> isLambdaActions = new ArrayList<>();
private Integer order = null; // optional order for withChoice or withJunction
}

View File

@@ -0,0 +1,18 @@
package click.kamil.springstatemachineexporter.ast.common;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FileUtils {
public static List<Path> findFilesWithExtension(Path startDir, String extension) throws IOException {
try (Stream<Path> paths = Files.walk(startDir)) {
return paths
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
.collect(Collectors.toList());
}
}
}

View File

@@ -0,0 +1,36 @@
package click.kamil.springstatemachineexporter.ast.in;
import org.eclipse.jdt.core.dom.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class AstFileFinder {
private final CompilationUnit cu;
public AstFileFinder(String source) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(false);
parser.setSource(source.toCharArray());
this.cu = (CompilationUnit) parser.createAST(null);
}
public boolean hasClassWithAnnotation(String source, String annotationName) {
AtomicBoolean found = new AtomicBoolean(false);
cu.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
if (node.isInterface()) return true;
for (Object modifier : node.modifiers()) {
if (modifier instanceof Annotation annotation &&
annotation.getTypeName().getFullyQualifiedName().equals(annotationName)) {
found.set(true);
return false;
}
}
return true;
}
});
return found.get();
}
}

View File

@@ -0,0 +1,168 @@
package click.kamil.springstatemachineexporter.ast.out;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Dot implements StateMachineExporter {
@Override
public String export(List<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.getType())) {
for (String source : t.getSourceStates()) {
statesWithChoice.add(simplify(source));
}
}
}
// 3. Declare normal states and choice pseudostates
Set<String> allStates = new HashSet<>();
for (Transition t : transitions) {
if (t.getSourceStates() != null) {
for (String s : t.getSourceStates()) allStates.add(simplify(s));
}
if (t.getTargetStates() != null) {
for (String tgt : t.getTargetStates()) allStates.add(simplify(tgt));
}
}
// Print all normal states (except choice pseudostates for now)
for (String state : allStates) {
if (!statesWithChoice.contains(state + "_choice")) {
String shape = endStates.contains(state) ? "doublecircle" : "circle";
sb.append(" ").append(state)
.append(" [shape=").append(shape).append(", fillcolor=lightgray];\n");
}
}
sb.append("\n");
// Declare choice pseudostates as diamonds
for (String state : statesWithChoice) {
String choiceNode = state + "_choice";
sb.append(" ").append(choiceNode)
.append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n");
}
sb.append("\n");
// 4. Connect start node(s) to start states
for (String start : startStates) {
sb.append(" start -> ").append(simplify(start)).append(";\n");
}
sb.append("\n");
// 5. Connect normal states to their choice pseudostates
for (String state : statesWithChoice) {
String choiceNode = state + "_choice";
sb.append(" ").append(state).append(" -> ").append(choiceNode).append(";\n");
}
sb.append("\n");
// 6. Transitions: reroute withChoice transitions via choice pseudostate
for (Transition t : transitions) {
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
List<String> targets;
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
if (!allowNoTarget) {
// self-loop: targets = sources
targets = t.getSourceStates();
} else {
// withJoin/withFork with no targets: skip
continue;
}
} else {
targets = t.getTargetStates();
}
for (String rawSource : t.getSourceStates()) {
String source = simplify(rawSource);
for (String rawTarget : targets) {
String target = simplify(rawTarget);
String edgeSource = source;
if ("withChoice".equals(t.getType()) && statesWithChoice.contains(source)) {
edgeSource = source + "_choice";
}
// Prepare guard text
String guardText = "";
if (t.getGuard() != null && !t.getGuard().isBlank()) {
if (useLambdaGuards && t.isLambdaGuard()) {
guardText = "lambda";
} else {
guardText = t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
}
}
// Prepare actions text
String actionsText = "";
if (t.getActions() != null && !t.getActions().isEmpty()) {
StringBuilder actionBuilder = new StringBuilder();
for (int i = 0; i < t.getActions().size(); i++) {
if (i > 0) actionBuilder.append(", ");
String action = t.getActions().get(i);
boolean isLambda = t.getIsLambdaActions().get(i);
if (useLambdaGuards && isLambda) {
actionBuilder.append("lambda");
} else {
actionBuilder.append(action);
}
}
actionsText = actionBuilder.toString();
}
// Build label with event, guard, and actions
StringBuilder label = new StringBuilder();
if (t.getEvent() != null && !t.getEvent().isBlank()) {
label.append(simplify(t.getEvent()));
}
if (!guardText.isBlank()) {
if (!label.isEmpty()) label.append(" ");
label.append("[").append(guardText).append("]");
}
if (!actionsText.isBlank()) {
if (!label.isEmpty()) label.append(" / ");
label.append(actionsText);
}
// Add order info for choice/junction transitions
if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) {
if (!label.isEmpty()) label.append(" ");
label.append("(order=").append(t.getOrder()).append(")");
}
String edgeLabel = !label.isEmpty() ? " [label=\"" + label.toString() + "\"]" : "";
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
}
}
}
sb.append("}\n");
return sb.toString();
}
@Override
public String getFileExtension() {
return ".dot";
}
}

View File

@@ -0,0 +1,182 @@
package click.kamil.springstatemachineexporter.ast.out;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import io.micrometer.common.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class PlantUml implements StateMachineExporter {
// Color palette for choices
private final List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
private String getColor(Transition t, String source, Map<String, String> choiceStateColorMap) {
return switch (t.getType()) {
case "withChoice" -> choiceStateColorMap.getOrDefault(source, "000000");
case "withExternal" -> "1E90FF";
case "withInternal" -> "32CD32";
case "withLocal" -> "FFA500";
case "withJunction" -> "FF69B4";
case "withJoin" -> "8A2BE2";
case "withFork" -> "20B2AA";
default -> "000000";
};
}
@Override
public String export(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
Set<String> statesWithChoice = new HashSet<>();
for (Transition t : transitions) {
if ("withChoice".equals(t.getType())) {
for (String source : t.getSourceStates()) {
statesWithChoice.add(simplify(source));
}
}
}
// 3. Declare choice pseudostates & connect from original state
for (String state : statesWithChoice) {
String choiceState = state + "_choice";
sb.append("state ").append(choiceState).append(" <<choice>>\n");
sb.append(state).append(" --> ").append(choiceState).append("\n");
}
sb.append("\n");
// Assign color to each choice state
Map<String, String> choiceStateColorMap = new HashMap<>();
int colorIndex = 0;
for (String state : statesWithChoice) {
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
colorIndex++;
}
Set<String> transitionLines = new HashSet<>();
// 4. Output transitions
for (Transition t : transitions) {
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
List<String> targets;
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
if (!allowNoTarget) {
targets = t.getSourceStates(); // Self-loop
} else {
continue;
}
} else {
targets = t.getTargetStates();
}
for (String rawSource : t.getSourceStates()) {
String source = simplify(rawSource);
for (String rawTarget : targets) {
String target = simplify(rawTarget);
String transitionSource = "withChoice".equals(t.getType()) && statesWithChoice.contains(source)
? source + "_choice"
: source;
String label = buildLabel(t, useLambdaGuards);
String color = getColor(t, source, choiceStateColorMap);
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
if (transitionLines.add(line)) {
sb.append(line).append("\n");
}
}
}
}
sb.append("\n");
// 5. Mark end states
for (String end : endStates) {
sb.append(simplify(end)).append(" --> [*]\n");
}
sb.append("@enduml\n");
return sb.toString();
}
@Override
public String getFileExtension() {
return ".plantuml.dot";
}
private String getGuardText(Transition t, boolean useLambdaGuards) {
String guard = t.getGuard();
if (StringUtils.isBlank(guard)) {
return "";
}
if (useLambdaGuards && t.isLambdaGuard()) {
return "lambda";
}
return guard
.replaceAll("[\\n\\r]+", " ")
.replaceAll("\\s{2,}", " ")
.trim();
}
private String getActionsText(Transition t, boolean useLambdaGuards) {
if (t.getActions() == null || t.getActions().isEmpty()) {
return "";
}
List<String> actions = t.getActions();
List<Boolean> isLambdaFlags = t.getIsLambdaActions();
// Defensive: ensure lists are same size to avoid IndexOutOfBoundsException
int size = Math.min(actions.size(), isLambdaFlags.size());
return IntStream.range(0, size)
.mapToObj(i -> (useLambdaGuards && isLambdaFlags.get(i)) ? "lambda" : actions.get(i))
.collect(Collectors.joining(", "));
}
private String buildLabel(Transition t, boolean useLambdaGuards) {
List<String> parts = new ArrayList<>();
Optional.ofNullable(t.getEvent())
.filter(e -> !e.isBlank())
.map(this::simplify)
.ifPresent(parts::add);
Optional.of(getGuardText(t, useLambdaGuards))
.filter(g -> !g.isBlank())
.map(g -> "[" + g + "]")
.ifPresent(parts::add);
Optional.of(getActionsText(t, useLambdaGuards))
.filter(a -> !a.isBlank())
.ifPresent(actions -> {
if (!parts.isEmpty()) {
int lastIndex = parts.size() - 1;
String last = parts.get(lastIndex);
parts.set(lastIndex, last + " / " + actions);
} else {
parts.add(actions);
}
});
Optional.ofNullable(t.getOrder())
.filter(order -> List.of("withChoice", "withJunction").contains(t.getType()))
.map(order -> "(order=" + order + ")")
.ifPresent(parts::add);
return String.join(" ", parts);
}
}

View File

@@ -0,0 +1,112 @@
package click.kamil.springstatemachineexporter.ast.out;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Scxml implements StateMachineExporter {
@Override
public String export(List<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.getSourceStates() != null) allStates.addAll(t.getSourceStates());
if (t.getTargetStates() != null) allStates.addAll(t.getTargetStates());
}
// 2. Emit <state> elements
for (String state : allStates) {
String simpleState = simplify(state);
sb.append(" <state id=\"").append(simpleState).append("\">\n");
if (startStates.contains(state)) {
sb.append(" <initial>\n");
sb.append(" <transition target=\"").append(simpleState).append("\"/>\n");
sb.append(" </initial>\n");
}
for (Transition t : transitions) {
if (t.getSourceStates() == null || !t.getSourceStates().contains(state)) continue;
boolean allowNoTarget = "withJoin".equals(t.getType()) || "withFork".equals(t.getType());
List<String> targets;
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
if (!allowNoTarget) {
targets = List.of(state); // self-loop
} else {
continue;
}
} else {
targets = t.getTargetStates();
}
for (String rawTarget : targets) {
String target = simplify(rawTarget);
if (target.isEmpty()) continue;
sb.append(renderTransition(t, target, useLambdaGuards));
}
}
sb.append(" </state>\n\n");
}
sb.append("</scxml>\n");
return sb.toString();
}
@Override
public String getFileExtension() {
return ".scxml.xml";
}
private static String getGuardText(Transition t, boolean useLambdaGuards) {
if (t.getGuard() == null || t.getGuard().isBlank()) return "";
if (useLambdaGuards && t.isLambdaGuard()) return "lambda";
return t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
}
private static String escapeXml(String s) {
if (s == null) return "";
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace("\"", "&quot;").replace("'", "&apos;");
}
private String renderTransition(Transition t, String target, boolean useLambdaGuards) {
StringBuilder sb = new StringBuilder(" <transition");
if (t.getEvent() != null && !t.getEvent().isBlank()) {
sb.append(" event=\"").append(simplify(t.getEvent())).append("\"");
}
sb.append(" target=\"").append(target).append("\"");
String guard = getGuardText(t, useLambdaGuards);
if (!guard.isBlank()) {
sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
}
// Choice/junction order → XML comment
if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) {
sb.append(">\n");
sb.append(" <!-- order=").append(t.getOrder()).append(" -->\n");
sb.append(" </transition>\n");
} else {
sb.append("/>\n");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,21 @@
package click.kamil.springstatemachineexporter.ast.out;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.List;
import java.util.Set;
public interface StateMachineExporter {
String export(List<Transition> transitions,
Set<String> startStates,
Set<String> endStates,
boolean useLambdaGuards);
// Helper: extract last enum part
default String simplify(String full) {
if (full == null) return "";
int dot = full.lastIndexOf('.');
return dot >= 0 ? full.substring(dot + 1) : full;
}
String getFileExtension(); // e.g. ".dot", ".plantuml.dot", ".scxml.xml"
}

View File

@@ -0,0 +1,180 @@
package click.kamil.springstatemachineexporter.springbased;
import click.kamil.springstatemachineexporter.statemachine.OrderEvents;
import click.kamil.springstatemachineexporter.statemachine.OrderStates;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.statemachine.StateMachine;
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.PrintWriter;
public class SpringExporter {
}
@Component
@Slf4j
@RequiredArgsConstructor
class Runner implements ApplicationRunner {
private final DotExporter dotExporter;
private final PlantUmlExporter plantUmlExporter;
private final ScxmlExporter scxmlExporter;
@Override
public void run(ApplicationArguments args) throws Exception {
dotExporter.export("stateMachine.dot");
plantUmlExporter.export("stateMachine.uml.dot");
scxmlExporter.export("stateMachine.scxml.xml");
}
}
abstract class BaseExporter {
@Qualifier("simpleEnumStateMachineFactory")
protected final StateMachineFactory<OrderStates, OrderEvents> factory;
protected BaseExporter(StateMachineFactory<OrderStates, OrderEvents> factory) {
this.factory = factory;
}
abstract void export(String name) throws Exception;
protected StateMachine<OrderStates, OrderEvents> createStateMachine() {
return factory.getStateMachine("order");
}
}
@Component
class DotExporter extends BaseExporter {
public DotExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
super(factory);
}
@Override
public void export(String filename) throws Exception {
try (PrintWriter writer = new PrintWriter(filename)) {
StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
writer.println("digraph stateMachine {");
for (State<OrderStates, OrderEvents> state : machine.getStates()) {
writer.printf(" %s;%n", state.getId());
}
for (Transition<OrderStates, OrderEvents> transition : machine.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("}");
}
}
}
@Component
class PlantUmlExporter extends BaseExporter {
public PlantUmlExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
super(factory);
}
@Override
public void export(String filename) throws Exception {
try (PrintWriter writer = new PrintWriter(filename)) {
StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
writer.println("@startuml");
writer.println("[*] --> " + machine.getInitialState().getId());
for (Transition<OrderStates, OrderEvents> transition : machine.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 (!label.isEmpty()) label += " ";
label += "[guard]";
}
writer.printf("%s --> %s%s%n", source, target, !label.isEmpty() ? " : " + label : "");
}
}
for (State<OrderStates, OrderEvents> state : machine.getStates()) {
if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.END) {
writer.printf("%s --> [*]%n", state.getId());
}
}
writer.println("@enduml");
}
}
}
@Component
class ScxmlExporter extends BaseExporter {
public ScxmlExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
super(factory);
}
@Override
public void export(String filename) throws Exception {
StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.newDocument();
Element scxml = doc.createElement("scxml");
scxml.setAttribute("version", "1.0");
doc.appendChild(scxml);
Element statesElement = doc.createElement("states");
scxml.appendChild(statesElement);
for (State<OrderStates, OrderEvents> state : machine.getStates()) {
Element stateEl = doc.createElement("state");
stateEl.setAttribute("id", state.getId().toString());
statesElement.appendChild(stateEl);
}
Element transitionsElement = doc.createElement("transitions");
scxml.appendChild(transitionsElement);
for (Transition<OrderStates, OrderEvents> transition : machine.getTransitions()) {
Element t = doc.createElement("transition");
t.setAttribute("from", transition.getSource().getId().toString());
t.setAttribute("to", transition.getTarget().getId().toString());
t.setAttribute("event", String.valueOf(transition.getTrigger()));
transitionsElement.appendChild(t);
}
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(new File(filename)));
}
}

View File

@@ -0,0 +1,159 @@
package click.kamil.springstatemachineexporter.statemachine;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
import java.util.EnumSet;
@Configuration
@EnableStateMachineFactory(name = "complexStateMachineFactory")
public class ComplexStateMachineConfig extends StateMachineConfigurerAdapter<ComplexStateMachineConfig.States, ComplexStateMachineConfig.Events> {
public enum States {
STATE1, STATE2, STATE3, STATE4, STATE5,
STATE6, STATE7, STATE8, STATE9, STATE10,
STATE11, STATE12, STATE13, STATE14, STATE15,
STATE16, STATE17, STATE18, STATE19, STATE20
}
public enum Events {
EVENT1, EVENT2, EVENT3, EVENT4, EVENT5,
EVENT6, EVENT7, EVENT8, EVENT9, EVENT10,
EVENT11, EVENT12, EVENT13, EVENT14, EVENT15
}
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.STATE1)
.states(EnumSet.allOf(States.class))
.stateEntry(States.STATE5, logEntryAction("Entered STATE5"))
.stateExit(States.STATE10, logExitAction("Exited STATE10"));
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
// External transitions
transitions
.withExternal().source(States.STATE1).target(States.STATE2).event(Events.EVENT1)
.and()
.withExternal().source(States.STATE2).target(States.STATE3).event(Events.EVENT2)
.and()
.withExternal().source(States.STATE3).target(States.STATE4).event(Events.EVENT3)
.and()
.withExternal().source(States.STATE4).target(States.STATE5).event(Events.EVENT4)
.and()
.withExternal().source(States.STATE5).target(States.STATE6).event(Events.EVENT5)
.and()
.withExternal().source(States.STATE6).target(States.STATE7).event(Events.EVENT6)
.and()
.withExternal().source(States.STATE7).target(States.STATE8).event(Events.EVENT7)
.and()
.withExternal().source(States.STATE8).target(States.STATE9).event(Events.EVENT8)
.and()
.withExternal().source(States.STATE9).target(States.STATE10).event(Events.EVENT9)
.and()
.withExternal().source(States.STATE10).target(States.STATE11).event(Events.EVENT10)
.and()
.withExternal().source(States.STATE11).target(States.STATE12).event(Events.EVENT11)
.and()
.withExternal().source(States.STATE12).target(States.STATE13).event(Events.EVENT12)
.and()
.withExternal().source(States.STATE13).target(States.STATE14).event(Events.EVENT13)
.and()
.withExternal().source(States.STATE14).target(States.STATE15).event(Events.EVENT14)
.and()
.withExternal().source(States.STATE15).target(States.STATE16).event(Events.EVENT15);
// Choice transitions (at least 10)
transitions
.withChoice()
.source(States.STATE16)
.first(States.STATE17, guardVarEquals("value1"))
.then(States.STATE18, guardVarEquals("value2"))
.last(States.STATE19)
.and()
.withChoice()
.source(States.STATE17)
.first(States.STATE20, guardEventHeaderEquals("header1", "foo"))
.last(States.STATE16)
.and()
.withChoice()
.source(States.STATE18)
.first(States.STATE19, guardVarEquals("value3"))
.last(States.STATE20)
.and()
.withChoice()
.source(States.STATE19)
.first(States.STATE1, guardVarEquals("reset"))
.last(States.STATE20)
.and()
.withChoice()
.source(States.STATE20)
.first(States.STATE5, guardEventHeaderEquals("header2", "bar"))
.last(States.STATE1)
.and()
.withChoice()
.source(States.STATE11)
.first(States.STATE13, guardVarEquals("goTo13"))
.then(States.STATE14, guardVarEquals("goTo14"))
.last(States.STATE15)
.and()
.withChoice()
.source(States.STATE12)
.first(States.STATE10, guardVarEquals("goBack10"))
.last(States.STATE11)
.and()
.withChoice()
.source(States.STATE14)
.first(States.STATE12, guardVarEquals("loop12"))
.last(States.STATE16)
.and()
.withChoice()
.source(States.STATE9)
.first(States.STATE8, guardVarEquals("stepBack"))
.then(States.STATE7, guardVarEquals("stepBackMore"))
.last(States.STATE6)
.and()
.withChoice()
.source(States.STATE8)
.first(States.STATE9, guardVarEquals("forward9"))
.last(States.STATE10);
}
private Guard<States, Events> guardVarEquals(String expected) {
return context -> {
Object var = context.getExtendedState().getVariables().get("var");
return expected.equals(var);
};
}
private Guard<States, Events> guardEventHeaderEquals(String headerName, String expected) {
return context -> {
Object header = context.getMessageHeader(headerName);
return expected.equals(header);
};
}
private Action<States, Events> logEntryAction(String msg) {
return context -> System.out.println(msg);
}
private Action<States, Events> logExitAction(String msg) {
return context -> System.out.println(msg);
}
}

View File

@@ -0,0 +1,100 @@
package click.kamil.springstatemachineexporter.statemachine;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
@Configuration
@EnableStateMachineFactory(name = "forkJoinStateMachineFactory")
public class ForkJoinStateMachineConfig extends StateMachineConfigurerAdapter<ForkJoinStateMachineConfig.States, ForkJoinStateMachineConfig.Events> {
public enum States {
// Main flow
START, FORK, JOIN, END,
// Region 1
REGION1_STATE1, REGION1_STATE2,
// Region 2
REGION2_STATE1, REGION2_STATE2
}
public enum Events {
TO_FORK, R1_NEXT, R2_NEXT, TO_JOIN, TO_END
}
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.START)
.state(States.FORK)
.fork(States.FORK)
.join(States.JOIN)
.end(States.END)
.and()
.withStates()
.parent(States.FORK)
.region("Region1")
.initial(States.REGION1_STATE1)
.state(States.REGION1_STATE2)
.and()
.withStates()
.parent(States.FORK)
.region("Region2")
.initial(States.REGION2_STATE1)
.state(States.REGION2_STATE2);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
// Initial path to fork
.withExternal()
.source(States.START).target(States.FORK).event(Events.TO_FORK)
.and()
// Fork: START → Region1 & Region2
.withFork()
.source(States.FORK)
.target(States.REGION1_STATE1)
.target(States.REGION2_STATE1)
.and()
// Region1 flow
.withExternal()
.source(States.REGION1_STATE1).target(States.REGION1_STATE2).event(Events.R1_NEXT)
.and()
// Region2 flow
.withExternal()
.source(States.REGION2_STATE1).target(States.REGION2_STATE2).event(Events.R2_NEXT)
.and()
// Join when both regions reach REGIONx_STATE2
.withJoin()
.source(States.REGION1_STATE2)
.source(States.REGION2_STATE2)
.target(States.JOIN)
.and()
// From join to end
.withExternal()
.source(States.JOIN).target(States.END).event(Events.TO_END);
}
private Action<States, Events> log(String message) {
return context -> System.out.println("ACTION: " + message);
}
private Guard<States, Events> alwaysTrue() {
return context -> true;
}
}

View File

@@ -0,0 +1,7 @@
package click.kamil.springstatemachineexporter.statemachine;
public enum OrderEvents {
FULFILL, PAY, CANCEL,
ABCD, NOPE, IGNORE
}

View File

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

View File

@@ -0,0 +1,103 @@
package click.kamil.springstatemachineexporter.statemachine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
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(name = "simpleEnumStateMachineFactory")
@Slf4j
class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
Guard<OrderStates, OrderEvents> guard1 = null;
Action<OrderStates, OrderEvents> action2 = null;
transitions
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY)
.and()
.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).action(new Action<OrderStates, OrderEvents>() {
@Override
public void execute(StateContext<OrderStates, OrderEvents> context) {
;
}
}).action(action2);
}
@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);
}
}