This commit is contained in:
2026-06-06 20:40:54 +02:00
parent 3449aead51
commit 38f960dc55
17 changed files with 814 additions and 87 deletions

View File

@@ -8,4 +8,5 @@ include ':state_machines:dynamic_state_machine'
include ':state_machines:enumstate_state_machine'
include ':state_machines:forkjoin_state_machine'
include ':state_machines:inheritance_state_machine'
include ':state_machines:inheritance_extra_functions_state_machine'
include ':state_machines:simple_state_machine'

View File

@@ -1,18 +1,20 @@
package click.kamil.springstatemachineexporter;
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
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.CodebaseContext;
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 org.eclipse.jdt.core.dom.TypeDeclaration;
import java.io.IOException;
import java.io.PrintWriter;
@@ -33,37 +35,74 @@ public class Main {
runExporter(Path.of(START_DIR), Path.of(OUTPUT_DIR));
}
static void processAnnotationFoundSource(String source, Path javaFile, List<StateMachineExporter> outputs, Path outputDir) throws IOException {
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());
public static void runExporter(Path inputDir, Path outputDir) throws IOException {
CodebaseContext context = new CodebaseContext();
context.scan(inputDir);
List<Transition> transitions = AstTransitionParser.parseTransitions(configureMethod);
for (Transition t : transitions) {
System.out.println(t);
List<StateMachineExporter> outputs = List.of(
new PlantUml(),
new Dot(),
new Scxml()
);
List<TypeDeclaration> entryPoints = context.findEntryPointClasses(TARGET_ANNOTATIONS);
for (TypeDeclaration td : entryPoints) {
processEntryPoint(td, outputs, outputDir, context);
}
// Still handle methods returning StateMachine for now (might be harder to refactor to CodebaseContext)
AstFileFinder finder = new AstFileFinder();
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(inputDir), EXTENSION);
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
if (finder.hasFunctionReturningType(source, STATE_MACHINE_RETURN_TYPES)) {
processReturnTypeFoundSource(source, javaFile, outputs, outputDir);
}
}
}
static void processEntryPoint(TypeDeclaration td, List<StateMachineExporter> outputs, Path outputDir, CodebaseContext context) throws IOException {
String className = td.getName().getFullyQualifiedName();
System.out.println("Processing state machine config: " + className);
StateMachineAggregator aggregator = new StateMachineAggregator(context);
List<Transition> transitions = aggregator.aggregateTransitions(td);
Set<String> startStatesTransitions = TransitionStateUtils.findStartStates(transitions);
Set<String> endStatesTransitions = TransitionStateUtils.findEndStates(transitions);
Set<String> initialStatesAst = new HashSet<>();
Set<String> endStatesAst = new HashSet<>();
aggregator.aggregateStates(td, initialStatesAst, endStatesAst);
System.out.println("Start States Ast: " + initialStatesAst);
System.out.println("End States Ast: " + endStatesAst);
Set<String> startStates = new HashSet<>(startStatesTransitions);
startStates.addAll(initialStatesAst);
Set<String> endStates = new HashSet<>(endStatesTransitions);
endStates.addAll(endStatesAst);
generateOutputs(outputDir, className, transitions, startStates, endStates, outputs);
}
static void processReturnTypeFoundSource(String source, Path javaFile, List<StateMachineExporter> outputs, Path outputDir) throws IOException {
Set<MethodDeclaration> methodsWithReturnType = new StateMachineTransitionConfigurationMethodFinder(source).findMethodsWithReturnType(STATE_MACHINE_RETURN_TYPES);
if (!methodsWithReturnType.isEmpty()) {
for (MethodDeclaration m : methodsWithReturnType) {
System.out.println("Found method returning StateMachine in: " + javaFile.toAbsolutePath());
List<Transition> transitions = AstTransitionParser.parseTransitions2(m);
Set<String> startStatesTransitions = TransitionStateUtils.findStartStates(transitions);
Set<String> endStatesTransitions = TransitionStateUtils.findEndStates(transitions);
Set<String> startStates = new HashSet<>(startStatesTransitions);
Set<String> endStates = new HashSet<>(endStatesTransitions);
String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", "");
generateOutputs(outputDir, baseName, transitions, startStates, endStates, outputs);
}
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("[.][^.]+$", "");
generateOutputs(outputDir, baseName, transitions, startStates, endStates, outputs);
}
}
@@ -83,59 +122,4 @@ public class Main {
}
}
}
public static void runExporter(Path inputDir, Path outputDir) throws IOException {
AstFileFinder finder = new AstFileFinder();
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(inputDir), EXTENSION);
List<StateMachineExporter> outputs = List.of(
new PlantUml(),
new Dot(),
new Scxml()
);
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
if (finder.hasClassWithAnnotation(source, TARGET_ANNOTATIONS)) {
processAnnotationFoundSource(source, javaFile, outputs, outputDir);
} else if (finder.hasFunctionReturningType(source, STATE_MACHINE_RETURN_TYPES)) {
processReturnTypeFoundSource(source, javaFile, outputs, outputDir);
}
}
}
static void processReturnTypeFoundSource(String source, Path javaFile, List<StateMachineExporter> outputs, Path outputDir) throws IOException {
Set<MethodDeclaration> methodsWithReturnType = new StateMachineTransitionConfigurationMethodFinder(source).findMethodsWithReturnType(STATE_MACHINE_RETURN_TYPES);
if (!methodsWithReturnType.isEmpty()) {
for (MethodDeclaration m : methodsWithReturnType) {
System.out.println("Found method returning StateMachine in: " + javaFile.toAbsolutePath());
System.out.println("Method body:\n" + m.getBody());
List<Transition> transitions = AstTransitionParser.parseTransitions2(m);
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("[.][^.]+$", "");
generateOutputs(outputDir, baseName, transitions, startStates, endStates, outputs);
}
}
}
}

View File

@@ -74,7 +74,7 @@ public class AstTransitionParser {
return chain;
}
private static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall) {
public static List<Transition> parseTransitionsFromExpression(MethodInvocation rootCall) {
List<Transition> transitions = new ArrayList<>();
// Step 1: Unroll method chain

View File

@@ -0,0 +1,243 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class StateMachineAggregator {
private final CodebaseContext context;
public StateMachineAggregator(CodebaseContext context) {
this.context = context;
}
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
List<Transition> allTransitions = new ArrayList<>();
Set<String> visitedClasses = new HashSet<>();
aggregateTransitionsRecursive(startClass, startClass, allTransitions, visitedClasses, new HashSet<>());
return allTransitions;
}
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodDeclaration> visitedMethods) {
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) return;
visitedClasses.add(currentTd.getName().getFullyQualifiedName());
// Process this class
MethodDeclaration configureMethod = findConfigureTransitionsMethod(currentTd);
if (configureMethod != null) {
allTransitions.addAll(parseTransitionsWithMethodCalls(configureMethod, searchStartClass, visitedMethods));
}
// Process superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
String superclassName = getSimpleName(superclassType);
TypeDeclaration superclassTd = context.getTypeDeclaration(superclassName);
if (superclassTd != null) {
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods);
}
}
}
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodDeclaration> visitedMethods) {
List<Transition> transitions = new ArrayList<>();
if (visitedMethods.contains(method)) return transitions;
visitedMethods.add(method);
Block body = method.getBody();
if (body == null) return transitions;
for (Object stmt : body.statements()) {
Expression expr = null;
if (stmt instanceof ExpressionStatement es) {
expr = es.getExpression();
} else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
expr = fragment.getInitializer();
}
}
}
if (expr instanceof MethodInvocation mi) {
if (isTransitionChain(mi)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi));
} else {
// Possible method call to follow
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod != null && isRelevantMethod(calledMethod)) {
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods));
}
}
}
}
return transitions;
}
private boolean isTransitionChain(MethodInvocation mi) {
String name = mi.getName().getIdentifier();
if (Set.of("withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction").contains(name)) {
return true;
}
Expression expr = mi.getExpression();
if (expr instanceof MethodInvocation next) {
return isTransitionChain(next);
}
return false;
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
if (td == null) return null;
for (MethodDeclaration method : td.getMethods()) {
if (method.getName().getIdentifier().equals(methodName)) {
return method;
}
}
Type superclassType = td.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
return findMethodInHierarchy(methodName, superclassTd);
}
return null;
}
private boolean isRelevantMethod(MethodDeclaration method) {
return true;
}
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods()) {
if (isConfigureTransitionsMethod(method)) return method;
}
return null;
}
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure")) return false;
List<?> params = method.parameters();
if (params.size() != 1) return false;
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(0);
String typeName = param.getType().toString();
return typeName.startsWith("StateMachineTransitionConfigurer");
}
public void aggregateStates(TypeDeclaration startClass, Set<String> initialStates, Set<String> endStates) {
Set<String> visitedClasses = new HashSet<>();
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, visitedClasses, new HashSet<>());
}
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodDeclaration> visitedMethods) {
if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) return;
visitedClasses.add(currentTd.getName().getFullyQualifiedName());
MethodDeclaration configureMethod = findConfigureStatesMethod(currentTd);
if (configureMethod != null) {
parseStatesWithMethodCalls(configureMethod, searchStartClass, initialStates, endStates, visitedMethods);
}
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType));
if (superclassTd != null) {
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods);
}
}
}
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodDeclaration> visitedMethods) {
if (visitedMethods.contains(method)) return;
visitedMethods.add(method);
Block body = method.getBody();
if (body == null) return;
for (Object stmt : body.statements()) {
Expression expr = null;
if (stmt instanceof ExpressionStatement es) {
expr = es.getExpression();
} else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
expr = fragment.getInitializer();
}
}
}
if (expr instanceof MethodInvocation mi) {
if (isWithStatesChain(mi)) {
parseStateChain(mi, initialStates, endStates);
} else {
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod != null) {
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods);
}
}
}
}
}
private boolean isWithStatesChain(MethodInvocation mi) {
if (mi.getName().getIdentifier().equals("withStates")) return true;
Expression expr = mi.getExpression();
if (expr instanceof MethodInvocation next) return isWithStatesChain(next);
return false;
}
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates) {
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.get(0);
if ("initial".equals(methodName)) {
if (!isInsideRegionOrFork(call)) {
initialStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
}
} else if ("end".equals(methodName)) {
endStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
}
}
}
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 MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods()) {
if (isConfigureStatesMethod(method)) return method;
}
return null;
}
private boolean isConfigureStatesMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure")) return false;
List<?> params = method.parameters();
if (params.size() != 1) return false;
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(0);
String typeName = param.getType().toString();
return typeName.startsWith("StateMachineStateConfigurer");
}
private String getSimpleName(Type type) {
if (type.isSimpleType()) return ((SimpleType) type).getName().getFullyQualifiedName();
if (type.isParameterizedType()) return getSimpleName(((ParameterizedType) type).getType());
return type.toString();
}
}

View File

@@ -0,0 +1,109 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class CodebaseContext {
private final Map<String, CompilationUnit> classes = new HashMap<>();
private final Map<String, String> classSources = new HashMap<>();
private final Map<String, Path> classPaths = new HashMap<>();
public void scan(Path rootDir) throws IOException {
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java");
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
CompilationUnit cu = parse(source);
for (Object type : cu.types()) {
if (type instanceof TypeDeclaration td) {
String className = td.getName().getFullyQualifiedName();
classes.put(className, cu);
classSources.put(className, source);
classPaths.put(className, javaFile);
}
}
}
}
private 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);
}
public TypeDeclaration getTypeDeclaration(String className) {
CompilationUnit cu = classes.get(className);
if (cu == null) return null;
for (Object type : cu.types()) {
if (type instanceof TypeDeclaration td && td.getName().getFullyQualifiedName().equals(className)) {
return td;
}
}
return null;
}
public Path getPath(String className) {
return classPaths.get(className);
}
public List<TypeDeclaration> findEntryPointClasses(List<String> targetAnnotations) {
List<TypeDeclaration> entryPoints = new ArrayList<>();
for (CompilationUnit cu : classes.values()) {
for (Object type : cu.types()) {
if (type instanceof TypeDeclaration td) {
if (isEntryPoint(td, targetAnnotations)) {
entryPoints.add(td);
}
}
}
}
return entryPoints;
}
private boolean isEntryPoint(TypeDeclaration td, List<String> targetAnnotations) {
boolean isAbstract = false;
// Has annotation
for (Object modifier : td.modifiers()) {
if (modifier instanceof Annotation annotation &&
targetAnnotations.contains(annotation.getTypeName().getFullyQualifiedName())) {
return true;
}
if (modifier instanceof Modifier m && m.isAbstract()) {
isAbstract = true;
}
}
// If it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly
if (!isAbstract && extendsStateMachineConfigurerAdapter(td)) {
return true;
}
return false;
}
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
Set<String> knownAdapters = Set.of("EnumStateMachineConfigurerAdapter", "StateMachineConfigurerAdapter");
Type superclass = td.getSuperclassType();
if (superclass == null) return false;
String superclassName = getSimpleName(superclass);
if (knownAdapters.contains(superclassName)) return true;
TypeDeclaration superTd = getTypeDeclaration(superclassName);
if (superTd != null) {
return extendsStateMachineConfigurerAdapter(superTd);
}
return false;
}
private String getSimpleName(Type type) {
if (type.isSimpleType()) return ((SimpleType) type).getName().getFullyQualifiedName();
if (type.isParameterizedType()) return getSimpleName(((ParameterizedType) type).getType());
return type.toString();
}
}

View File

@@ -41,6 +41,12 @@ class RegressionTest {
Path.of("../state_machines/simple_state_machine"),
Path.of("src/test/resources/golden/SimpleEnumStateMachineConfiguration"),
"SimpleEnumStateMachineConfiguration"
),
new TestScenario(
"One State Machine (Inheritance)",
Path.of("../state_machines/inheritance_state_machine"),
Path.of("src/test/resources/golden/OneStateMachineConfiguration"),
"OneStateMachineConfiguration"
)
);
}

View File

@@ -0,0 +1,45 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
#### added
*.png
*.dot
*.scxml.xml
Combined.java

View File

@@ -0,0 +1,42 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'click.kamil'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}

View File

@@ -0,0 +1,203 @@
package click.kamil.examples.statemachine.inheritanceextrafunctionsstate;
import org.springframework.statemachine.action.Action;
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.config.configurers.ExternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.StateConfigurer;
import org.springframework.statemachine.guard.Guard;
abstract class AbstractOneStateMachineConfiguration extends AbstractStateMachineConfiguration<States, Events> {
protected abstract void addAdditionalStates(StateConfigurer<States, Events> states);
protected abstract void addAdditionalCancellationTransitions(StateMachineTransitionConfigurer<States, Events> transitions);
protected abstract void addAdditionalTransitions(ExternalTransitionConfigurer<States, Events> transitions) throws Exception;
protected void addAllCancellationTransitionsForSource(StateMachineTransitionConfigurer<States, Events> transitions, States source) {
addCancellationTransitionForCancelEvents(transitions, source, Events.EVENT_CANCEL, Events.EVENT_CANCEL_2);
}
private void addCancellationTransitionForCancelEvents(
StateMachineTransitionConfigurer<States, Events> transitions,
States source,
Events... cancelEvents) {
addTransitionForMultipleEvents(
transitions,
source,
States.CANCEL,
transitionConfigurer -> transitionConfigurer.action((context) -> System.out.println(context.getEvent().toString() + " " + context.getTransition().toString())),
cancelEvents
);
}
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
StateConfigurer<States, Events> end = states
.withStates()
.initial(States.STATE1)
.state(States.STATE2)
.state(States.STATE3)
.state(States.STATE4)
.state(States.STATE5)
.state(States.STATE6)
.state(States.STATE7)
.state(States.STATE8)
.state(States.STATE9)
.state(States.STATE10)
.state(States.STATE11)
.state(States.STATE12)
.state(States.STATE13)
.state(States.STATE14)
.state(States.STATE15)
.state(States.STATE16)
.state(States.STATE17)
.state(States.STATE18)
.state(States.STATE19)
.state(States.STATE20)
.state(States.STATEX)
.choice(States.STATEY)
.end(States.STATEZ);
addAdditionalStates(end);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
// External transitions
ExternalTransitionConfigurer<States, Events> t1 = 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);
transitions.withExternal().source(States.STATE1).target(States.CANCEL).event(Events.EVENT_CANCEL)
.and()
.withExternal().source(States.STATE2).target(States.CANCEL).event(Events.EVENT_CANCEL)
.and()
.withExternal().source(States.STATE3).target(States.CANCEL).event(Events.EVENT_CANCEL)
.and()
.withExternal().source(States.STATE4).target(States.CANCEL).event(Events.EVENT_CANCEL)
.and()
.withExternal().source(States.STATE5).target(States.CANCEL).event(Events.EVENT_CANCEL);
// EXTRA CHOICE
transitions.withExternal().source(States.STATE4).target(States.STATEY).event(Events.EVENTY);
transitions.withChoice().source(States.STATEY).first(States.STATEX, guardVarEquals("value1")).last(States.STATEZ);
// 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);
addAdditionalTransitions(t1);
addAdditionalCancellationTransitions(transitions);
}
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,27 @@
package click.kamil.examples.statemachine.inheritanceextrafunctionsstate;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
import java.util.function.Consumer;
import java.util.stream.Stream;
class AbstractStateMachineConfiguration<S,E> extends StateMachineConfigurerAdapter<S,E> {
protected void addTransitionForMultipleEvents(
StateMachineTransitionConfigurer<S, E> transitions,
S source,
S target,
Consumer<ExternalTransitionConfigurer<S,E>> transitionCustomizer,
E... events) {
Stream.of(events).forEach(event -> {
try {
ExternalTransitionConfigurer<S, E> transitionConfigurer =
transitions.withExternal().event(event).source(source).target(target);
transitionCustomizer.accept(transitionConfigurer);
} catch (Exception e) {
throw new IllegalStateException(e);
}
});
}
}

View File

@@ -0,0 +1,8 @@
package click.kamil.examples.statemachine.inheritanceextrafunctionsstate;
public enum Events {
EVENT1, EVENT2, EVENT3, EVENT4, EVENT5,
EVENT6, EVENT7, EVENT8, EVENT9, EVENT10,
EVENT11, EVENT12, EVENT13, EVENT14, EVENT15,
EVENTX, EVENTY, EVENTZ, EVENT_CANCEL, EVENT_CANCEL_2
}

View File

@@ -0,0 +1,27 @@
package click.kamil.examples.statemachine.inheritanceextrafunctionsstate;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.StateConfigurer;
class OneStateMachineConfiguration extends AbstractOneStateMachineConfiguration {
@Override
protected void addAdditionalStates(StateConfigurer<States, Events> states) {
states.state(States.STATE_EXTRA_1);
}
@Override
protected void addAdditionalCancellationTransitions(StateMachineTransitionConfigurer<States, Events> transitions) {
addAllCancellationTransitionsForSource(transitions, States.STATE_EXTRA_1);
}
@Override
protected void addAdditionalTransitions(ExternalTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
.and()
.withExternal().event(Events.EVENTX)
.source(States.STATE5)
.target(States.STATE_EXTRA_1);
}
}

View File

@@ -0,0 +1,9 @@
package click.kamil.examples.statemachine.inheritanceextrafunctionsstate;
public enum States {
STATE1, STATE2, STATE3, STATE4, STATE5,
STATE6, STATE7, STATE8, STATE9, STATE10,
STATE11, STATE12, STATE13, STATE14, STATE15,
STATE16, STATE17, STATE18, STATE19, STATE20,
STATEX, STATEY, STATEZ, STATE_EXTRA_1, CANCEL
}

View File

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

View File

@@ -0,0 +1 @@
spring.application.name=statemachinedemo

View File

@@ -0,0 +1,11 @@
package click.kamil.examples.statemachine;
import org.junit.jupiter.api.Test;
class MyTest {
@Test
void contextLoads() {
}
}

View File

@@ -87,7 +87,7 @@ abstract class AbstractOneStateMachineConfiguration extends AbstractStateMachine
.withExternal().source(States.STATE15).target(States.STATE16).event(Events.EVENT15);
// EXTRA CHOICE
transitions.withExternal().source(States.STATE4).target(States.STATEY).event(Events.EVENTY);
transitions.withChoice().source(States.STATEY).first(States.STATEX, guardVarEquals("value1")).last(States.STATEZ);
// Choice transitions (at least 10)