From 38f960dc55a5a7124426d52c2bb8136dfbe55ebe Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 6 Jun 2026 20:40:54 +0200 Subject: [PATCH] test --- settings.gradle | 1 + .../springstatemachineexporter/Main.java | 154 +++++------ .../ast/app/AstTransitionParser.java | 2 +- .../ast/app/StateMachineAggregator.java | 243 ++++++++++++++++++ .../ast/common/CodebaseContext.java | 109 ++++++++ .../RegressionTest.java | 6 + .../.gitignore | 45 ++++ .../build.gradle | 42 +++ .../AbstractOneStateMachineConfiguration.java | 203 +++++++++++++++ .../AbstractStateMachineConfiguration.java | 27 ++ .../Events.java | 8 + .../OneStateMachineConfiguration.java | 27 ++ .../States.java | 9 + .../app/StateMachineApplication.java | 11 + .../src/main/resources/application.properties | 1 + .../kamil/examples/statemachine/MyTest.java | 11 + .../AbstractOneStateMachineConfiguration.java | 2 +- 17 files changed, 814 insertions(+), 87 deletions(-) create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregator.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java create mode 100644 state_machines/inheritance_extra_functions_state_machine/.gitignore create mode 100644 state_machines/inheritance_extra_functions_state_machine/build.gradle create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractOneStateMachineConfiguration.java create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractStateMachineConfiguration.java create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/Events.java create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/OneStateMachineConfiguration.java create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/States.java create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/app/StateMachineApplication.java create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/main/resources/application.properties create mode 100644 state_machines/inheritance_extra_functions_state_machine/src/test/java/click/kamil/examples/statemachine/MyTest.java diff --git a/settings.gradle b/settings.gradle index 53ed2b9..cb5dc22 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java index f8c9b9e..81bd62d 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java @@ -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 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 transitions = AstTransitionParser.parseTransitions(configureMethod); - for (Transition t : transitions) { - System.out.println(t); + List outputs = List.of( + new PlantUml(), + new Dot(), + new Scxml() + ); + + List 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 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 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 transitions = aggregator.aggregateTransitions(td); + + Set startStatesTransitions = TransitionStateUtils.findStartStates(transitions); + Set endStatesTransitions = TransitionStateUtils.findEndStates(transitions); + + Set initialStatesAst = new HashSet<>(); + Set endStatesAst = new HashSet<>(); + aggregator.aggregateStates(td, initialStatesAst, endStatesAst); + + System.out.println("Start States Ast: " + initialStatesAst); + System.out.println("End States Ast: " + endStatesAst); + + Set startStates = new HashSet<>(startStatesTransitions); + startStates.addAll(initialStatesAst); + + Set endStates = new HashSet<>(endStatesTransitions); + endStates.addAll(endStatesAst); + + generateOutputs(outputDir, className, transitions, startStates, endStates, outputs); + } + + static void processReturnTypeFoundSource(String source, Path javaFile, List outputs, Path outputDir) throws IOException { + Set 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 transitions = AstTransitionParser.parseTransitions2(m); + + Set startStatesTransitions = TransitionStateUtils.findStartStates(transitions); + Set endStatesTransitions = TransitionStateUtils.findEndStates(transitions); + + Set startStates = new HashSet<>(startStatesTransitions); + Set endStates = new HashSet<>(endStatesTransitions); + + String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", ""); + generateOutputs(outputDir, baseName, transitions, startStates, endStates, outputs); } - - Set startStatesTransitions = TransitionStateUtils.findStartStates(transitions); - Set 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 startStates = new HashSet<>(startStatesTransitions); - startStates.addAll(visitor.getInitialStates()); - - Set 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 javaFiles = FileUtils.findFilesWithExtension(Set.of(inputDir), EXTENSION); - - List 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 outputs, Path outputDir) throws IOException { - Set 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 transitions = AstTransitionParser.parseTransitions2(m); - for (Transition t : transitions) { - System.out.println(t); - } - - Set startStatesTransitions = TransitionStateUtils.findStartStates(transitions); - Set 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 startStates = new HashSet<>(startStatesTransitions); - startStates.addAll(visitor.getInitialStates()); - - Set endStates = new HashSet<>(endStatesTransitions); - endStates.addAll(visitor.getEndStates()); - - String baseName = javaFile.getFileName().toString().replaceFirst("[.][^.]+$", ""); - generateOutputs(outputDir, baseName, transitions, startStates, endStates, outputs); - } - } - } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java index aa54f85..8acac39 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java @@ -74,7 +74,7 @@ public class AstTransitionParser { return chain; } - private static List parseTransitionsFromExpression(MethodInvocation rootCall) { + public static List parseTransitionsFromExpression(MethodInvocation rootCall) { List transitions = new ArrayList<>(); // Step 1: Unroll method chain diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregator.java new file mode 100644 index 0000000..3d3209d --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregator.java @@ -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 aggregateTransitions(TypeDeclaration startClass) { + List allTransitions = new ArrayList<>(); + Set visitedClasses = new HashSet<>(); + aggregateTransitionsRecursive(startClass, startClass, allTransitions, visitedClasses, new HashSet<>()); + return allTransitions; + } + + private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List allTransitions, Set visitedClasses, Set 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 parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set visitedMethods) { + List 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 initialStates, Set endStates) { + Set visitedClasses = new HashSet<>(); + aggregateStatesRecursive(startClass, startClass, initialStates, endStates, visitedClasses, new HashSet<>()); + } + + private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedClasses, Set 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 initialStates, Set endStates, Set 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 initialStates, Set endStates) { + List 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(); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java new file mode 100644 index 0000000..f19623e --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -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 classes = new HashMap<>(); + private final Map classSources = new HashMap<>(); + private final Map classPaths = new HashMap<>(); + + public void scan(Path rootDir) throws IOException { + List 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 findEntryPointClasses(List targetAnnotations) { + List 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 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 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(); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java index ff0a73d..08fe6ac 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java @@ -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" ) ); } diff --git a/state_machines/inheritance_extra_functions_state_machine/.gitignore b/state_machines/inheritance_extra_functions_state_machine/.gitignore new file mode 100644 index 0000000..c022c2a --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/.gitignore @@ -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 \ No newline at end of file diff --git a/state_machines/inheritance_extra_functions_state_machine/build.gradle b/state_machines/inheritance_extra_functions_state_machine/build.gradle new file mode 100644 index 0000000..0f14971 --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/build.gradle @@ -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() +} diff --git a/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractOneStateMachineConfiguration.java b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractOneStateMachineConfiguration.java new file mode 100644 index 0000000..9e68f5f --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractOneStateMachineConfiguration.java @@ -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 { + + protected abstract void addAdditionalStates(StateConfigurer states); + + protected abstract void addAdditionalCancellationTransitions(StateMachineTransitionConfigurer transitions); + + protected abstract void addAdditionalTransitions(ExternalTransitionConfigurer transitions) throws Exception; + + protected void addAllCancellationTransitionsForSource(StateMachineTransitionConfigurer transitions, States source) { + addCancellationTransitionForCancelEvents(transitions, source, Events.EVENT_CANCEL, Events.EVENT_CANCEL_2); + } + + private void addCancellationTransitionForCancelEvents( + StateMachineTransitionConfigurer 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 config) throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + StateConfigurer 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 transitions) throws Exception { + + // External transitions + ExternalTransitionConfigurer 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 guardVarEquals(String expected) { + return context -> { + Object var = context.getExtendedState().getVariables().get("var"); + return expected.equals(var); + }; + } + + private Guard guardEventHeaderEquals(String headerName, String expected) { + return context -> { + Object header = context.getMessageHeader(headerName); + return expected.equals(header); + }; + } + + private Action logEntryAction(String msg) { + return context -> System.out.println(msg); + } + + private Action logExitAction(String msg) { + return context -> System.out.println(msg); + } +} diff --git a/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractStateMachineConfiguration.java b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractStateMachineConfiguration.java new file mode 100644 index 0000000..dc140ef --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/AbstractStateMachineConfiguration.java @@ -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 extends StateMachineConfigurerAdapter { + protected void addTransitionForMultipleEvents( + StateMachineTransitionConfigurer transitions, + S source, + S target, + Consumer> transitionCustomizer, + E... events) { + Stream.of(events).forEach(event -> { + try { + ExternalTransitionConfigurer transitionConfigurer = + transitions.withExternal().event(event).source(source).target(target); + transitionCustomizer.accept(transitionConfigurer); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + } +} diff --git a/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/Events.java b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/Events.java new file mode 100644 index 0000000..8e9667f --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/Events.java @@ -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 +} diff --git a/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/OneStateMachineConfiguration.java b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/OneStateMachineConfiguration.java new file mode 100644 index 0000000..b7a36bc --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/OneStateMachineConfiguration.java @@ -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) { + states.state(States.STATE_EXTRA_1); + } + + @Override + protected void addAdditionalCancellationTransitions(StateMachineTransitionConfigurer transitions) { + addAllCancellationTransitionsForSource(transitions, States.STATE_EXTRA_1); + } + + @Override + protected void addAdditionalTransitions(ExternalTransitionConfigurer transitions) throws Exception { + transitions + .and() + .withExternal().event(Events.EVENTX) + .source(States.STATE5) + .target(States.STATE_EXTRA_1); + } + +} diff --git a/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/States.java b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/States.java new file mode 100644 index 0000000..f6cbeb9 --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/States.java @@ -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 +} diff --git a/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/app/StateMachineApplication.java b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/app/StateMachineApplication.java new file mode 100644 index 0000000..0bd0273 --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/main/java/click/kamil/examples/statemachine/inheritanceextrafunctionsstate/app/StateMachineApplication.java @@ -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); + } +} diff --git a/state_machines/inheritance_extra_functions_state_machine/src/main/resources/application.properties b/state_machines/inheritance_extra_functions_state_machine/src/main/resources/application.properties new file mode 100644 index 0000000..1cd01ff --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=statemachinedemo diff --git a/state_machines/inheritance_extra_functions_state_machine/src/test/java/click/kamil/examples/statemachine/MyTest.java b/state_machines/inheritance_extra_functions_state_machine/src/test/java/click/kamil/examples/statemachine/MyTest.java new file mode 100644 index 0000000..95af6f1 --- /dev/null +++ b/state_machines/inheritance_extra_functions_state_machine/src/test/java/click/kamil/examples/statemachine/MyTest.java @@ -0,0 +1,11 @@ +package click.kamil.examples.statemachine; + +import org.junit.jupiter.api.Test; + +class MyTest { + + @Test + void contextLoads() { + } + +} diff --git a/state_machines/inheritance_state_machine/src/main/java/click/kamil/examples/statemachine/inheritancestate/AbstractOneStateMachineConfiguration.java b/state_machines/inheritance_state_machine/src/main/java/click/kamil/examples/statemachine/inheritancestate/AbstractOneStateMachineConfiguration.java index 3e876f9..2bf8b5b 100644 --- a/state_machines/inheritance_state_machine/src/main/java/click/kamil/examples/statemachine/inheritancestate/AbstractOneStateMachineConfiguration.java +++ b/state_machines/inheritance_state_machine/src/main/java/click/kamil/examples/statemachine/inheritancestate/AbstractOneStateMachineConfiguration.java @@ -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)