package change
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("^\"|\"$", "");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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("&", "&").replace("<", "<").replace(">", ">")
|
||||
.replace("\"", """).replace("'", "'");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.statemachine;
|
||||
|
||||
public enum OrderEvents {
|
||||
FULFILL, PAY, CANCEL,
|
||||
|
||||
ABCD, NOPE, IGNORE
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package click.kamil.springstatemachineexporter.statemachine;
|
||||
|
||||
public enum OrderStates {
|
||||
SUBMITTED, PAID, FULFILLED, CANCELED
|
||||
,PAID1,PAID2, PAID3, INVALID, HAPPEN, SKIPPED, NEVER
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user