From b268639074b1094a3489e3aa2e541e834835c880 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 7 Jun 2026 09:25:22 +0200 Subject: [PATCH] updated for functions --- state_machine_exporter/build.gradle | 5 + .../springstatemachineexporter/Main.java | 16 +- .../ast/app/AstTransitionParser.java | 28 ++- .../ast/app/StateMachineAggregator.java | 233 +++++++++++++----- .../ast/app/TransitionStateUtils.java | 14 ++ .../ast/common/CodebaseContext.java | 28 ++- .../ast/out/PlantUml.java | 2 +- .../RegressionTest.java | 1 + .../ast/app/StateMachineAggregatorTest.java | 14 +- .../ComplexStateMachineConfig.puml | 59 +++++ .../ForkJoinStateMachineConfig.puml | 20 ++ .../KamilEnumStateMachineConfiguration.puml | 27 ++ .../OneStateMachineConfiguration.puml | 65 +++++ .../SimpleEnumStateMachineConfiguration.puml | 29 +++ .../TwoStateMachineConfiguration.puml | 72 ++++++ 15 files changed, 524 insertions(+), 89 deletions(-) create mode 100644 state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml create mode 100644 state_machine_exporter/src/test/resources/golden/ForkJoinStateMachineConfig/ForkJoinStateMachineConfig.puml create mode 100644 state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml create mode 100644 state_machine_exporter/src/test/resources/golden/OneStateMachineConfiguration/OneStateMachineConfiguration.puml create mode 100644 state_machine_exporter/src/test/resources/golden/SimpleEnumStateMachineConfiguration/SimpleEnumStateMachineConfiguration.puml create mode 100644 state_machine_exporter/src/test/resources/golden/TwoStateMachineExtraFunctionsConfiguration/TwoStateMachineConfiguration.puml diff --git a/state_machine_exporter/build.gradle b/state_machine_exporter/build.gradle index 0ee7e13..46ded9c 100644 --- a/state_machine_exporter/build.gradle +++ b/state_machine_exporter/build.gradle @@ -1,10 +1,15 @@ plugins { id 'java' + id 'application' } group = 'click.kamil.springstatemachineexporter' version = '0.0.1-SNAPSHOT' +application { + mainClass = 'click.kamil.springstatemachineexporter.Main' +} + java { toolchain { languageVersion = JavaLanguageVersion.of(21) 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 be23d27..1d22e55 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 @@ -68,21 +68,15 @@ public class Main { 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); + aggregator.aggregateStates(td); + Set initialStatesAst = aggregator.getInitialStates(); + Set endStatesAst = aggregator.getEndStates(); 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); + Set startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst); + Set endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst); generateOutputs(outputDir, className, 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 28b2587..dba8697 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 @@ -56,6 +56,10 @@ public class AstTransitionParser { } public static List parseTransitionsFromExpression(MethodInvocation rootCall, Map argsMap) { + return parseTransitionsFromExpression(rootCall, argsMap, false); + } + + public static List parseTransitionsFromExpression(MethodInvocation rootCall, Map argsMap, boolean isContinuation) { List transitions = new ArrayList<>(); // Step 1: Unroll method chain @@ -82,14 +86,32 @@ public class AstTransitionParser { if (!currentSegment.isEmpty()) segments.add(currentSegment); // Step 3: Parse each segment - for (List segment : segments) { + for (int i = 0; i < segments.size(); i++) { + List segment = segments.get(i); boolean isChoice = segment.stream().anyMatch(mi -> "withChoice".equals(mi.getName().getIdentifier())); boolean isJunction = segment.stream().anyMatch(mi -> "withJunction".equals(mi.getName().getIdentifier())); + // If it's a continuation and we are at the first segment, we should be careful + boolean isFirstSegmentOfContinuation = isContinuation && i == 0; + if (isChoice || isJunction) { - transitions.addAll(parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction", argsMap)); + List choiceTransitions = parseChoiceOrJunctionTransitions(segment, isChoice ? "withChoice" : "withJunction", argsMap); + if (isFirstSegmentOfContinuation) { + // For choice continuations, the first transition often has an empty source (inherited from receiver) + // We keep them as standard choice parsing handles it, but we ensure we don't introduce new start states + } + transitions.addAll(choiceTransitions); } else { - transitions.add(parseStandardTransition(segment, argsMap)); + Transition t = parseStandardTransition(segment, argsMap); + // If it's a continuation's first segment and it has no source, it's just refining the receiver + if (isFirstSegmentOfContinuation && t.getSourceStates().isEmpty()) { + // Don't add it as a new transition if it doesn't have source/target + if (!t.getTargetStates().isEmpty()) { + transitions.add(t); + } + } else if (!t.getSourceStates().isEmpty() || !t.getTargetStates().isEmpty() || t.getType() != null) { + transitions.add(t); + } } } 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 index ffb2cf2..39c35e7 100644 --- 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 @@ -2,6 +2,7 @@ package click.kamil.springstatemachineexporter.ast.app; import click.kamil.springstatemachineexporter.ast.app.domain.Transition; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.Getter; import org.eclipse.jdt.core.dom.*; import java.util.*; @@ -19,7 +20,7 @@ public class StateMachineAggregator { return allTransitions; } - private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List allTransitions, Set visitedClasses, Set visitedMethods, Map argsMap) { + private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List allTransitions, Set visitedClasses, Set visitedMethods, Map argsMap) { if (currentTd == null || visitedClasses.contains(currentTd.getName().getFullyQualifiedName())) return; visitedClasses.add(currentTd.getName().getFullyQualifiedName()); @@ -28,48 +29,62 @@ public class StateMachineAggregator { allTransitions.addAll(parseTransitionsWithMethodCalls(configureMethod, searchStartClass, visitedMethods, argsMap)); } + // Hierarchy - Superclass Type superclassType = currentTd.getSuperclassType(); if (superclassType != null) { - String superclassName = getSimpleName(superclassType); - TypeDeclaration superclassTd = context.getTypeDeclaration(superclassName); + TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType)); if (superclassTd != null) { aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap); } } - } - private List parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap) { - 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()) { - if (stmt instanceof ExpressionStatement es) { - transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap)); - } else if (stmt instanceof VariableDeclarationStatement vds) { - for (Object fragmentObj : vds.fragments()) { - if (fragmentObj instanceof VariableDeclarationFragment fragment) { - transitions.addAll(parseTransitionsInExpression(fragment.getInitializer(), searchStartClass, visitedMethods, argsMap)); - } + // Hierarchy - Interfaces + for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) { + if (interfaceTypeObj instanceof Type interfaceType) { + TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType)); + if (interfaceTd != null) { + aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap); } - } else if (stmt instanceof EnhancedForStatement efs) { - transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, true, null, null)); } } - return transitions; } - private List parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap) { + private record MethodVisit(MethodDeclaration method, Map args) {} + + private List parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap) { + MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap)); + if (visitedMethods.contains(visit)) { + return new ArrayList<>(); + } + visitedMethods.add(visit); + + Block body = method.getBody(); + if (body == null) { + return new ArrayList<>(); + } + + return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap); + } + + private List parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap) { List transitions = new ArrayList<>(); if (expr instanceof MethodInvocation mi) { - if (isTransitionChain(mi)) { + if (isTransitionChainEntry(mi)) { transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap))); + } else if (isTransitionChainContinuation(mi, argsMap)) { + transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true)); } else if (isForEach(mi)) { transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, true, null, null)); } else { + // Check if it's a call on a lambda parameter or a variable that resolved to a lambda + Expression receiver = mi.getExpression(); + if (receiver instanceof SimpleName sn) { + Object resolved = argsMap.get(sn.getIdentifier()); + if (resolved instanceof LambdaExpression lambda) { + transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, true, null, null)); + } + } + MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass); if (calledMethod != null) { Map nextArgsMap = buildArgsMap(calledMethod, mi, argsMap); @@ -84,7 +99,29 @@ public class StateMachineAggregator { return mi.getName().getIdentifier().equals("forEach"); } - private List parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap, boolean isTransitions, Set initialStates, Set endStates) { + private List parseLambdaInvocation(LambdaExpression lambda, List arguments, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap, boolean isTransitions, Set initialStates, Set endStates) { + Map lambdaArgsMap = new HashMap<>(argsMap); + List params = lambda.parameters(); + for (int i = 0; i < params.size() && i < arguments.size(); i++) { + Object p = params.get(i); + String paramName = ""; + if (p instanceof VariableDeclaration vp) paramName = vp.getName().getIdentifier(); + if (!paramName.isEmpty()) { + lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap)); + } + } + + if (isTransitions) { + if (lambda.getBody() instanceof Block block) return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap); + else if (lambda.getBody() instanceof Expression lambdaExpr) return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap); + } else { + if (lambda.getBody() instanceof Block block) parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap); + else if (lambda.getBody() instanceof Expression lambdaExpr) parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap); + } + return Collections.emptyList(); + } + + private List parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap, boolean isTransitions, Set initialStates, Set endStates) { List transitions = new ArrayList<>(); Expression receiver = forEachMi.getExpression(); List elements = unrollCollection(receiver, argsMap); @@ -109,7 +146,7 @@ public class StateMachineAggregator { return transitions; } - private List parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap, boolean isTransitions, Set initialStates, Set endStates) { + private List parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap, boolean isTransitions, Set initialStates, Set endStates) { List transitions = new ArrayList<>(); List elements = unrollCollection(efs.getExpression(), argsMap); String paramName = efs.getParameter().getName().getIdentifier(); @@ -135,6 +172,13 @@ public class StateMachineAggregator { Expression receiver = mi.getExpression(); if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List"))) || (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) { + if (mi.arguments().size() == 1) { + Expression arg = (Expression) mi.arguments().get(0); + if (arg instanceof SimpleName sn) { + Object resolved = argsMap.get(sn.getIdentifier()); + if (resolved instanceof List list) return (List) list; + } + } return (List) mi.arguments(); } } @@ -155,27 +199,62 @@ public class StateMachineAggregator { return ""; } - private List parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap) { + private List parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set visitedMethods, Map argsMap) { List transitions = new ArrayList<>(); for (Object stmt : block.statements()) { if (stmt instanceof ExpressionStatement es) transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap)); + else if (stmt instanceof VariableDeclarationStatement vds) { + for (Object fragmentObj : vds.fragments()) { + if (fragmentObj instanceof VariableDeclarationFragment fragment) { + Expression initializer = fragment.getInitializer(); + if (initializer != null) { + Object resolved = resolve(initializer, argsMap); + if (resolved != null) argsMap.put(fragment.getName().getIdentifier(), resolved); + transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap)); + } + } + } + } else if (stmt instanceof EnhancedForStatement efs) transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, true, null, null)); + else if (stmt instanceof TryStatement ts) transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap)); } return transitions; } - private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedMethods, Map argsMap) { + private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedMethods, Map argsMap) { for (Object stmt : block.statements()) { if (stmt instanceof ExpressionStatement es) parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap); + else if (stmt instanceof VariableDeclarationStatement vds) { + for (Object fragmentObj : vds.fragments()) { + if (fragmentObj instanceof VariableDeclarationFragment fragment) { + Expression initializer = fragment.getInitializer(); + if (initializer != null) { + Object resolved = resolve(initializer, argsMap); + if (resolved != null) argsMap.put(fragment.getName().getIdentifier(), resolved); + parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap); + } + } + } + } else if (stmt instanceof EnhancedForStatement efs) parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates); + else if (stmt instanceof TryStatement ts) parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap); } } - private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedMethods, Map argsMap) { + private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedMethods, Map argsMap) { if (expr instanceof MethodInvocation mi) { - if (isWithStatesChain(mi)) parseStateChain(mi, initialStates, endStates, argsMap); + if (isWithStatesChain(mi, argsMap)) parseStateChain(mi, initialStates, endStates, argsMap); else if (isForEach(mi)) parseForEach(mi, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates); else { + // Check if it's a call on a lambda parameter or a variable that resolved to a lambda + Expression receiver = mi.getExpression(); + if (receiver instanceof SimpleName sn) { + Object resolved = argsMap.get(sn.getIdentifier()); + if (resolved instanceof LambdaExpression lambda) { + parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, false, initialStates, endStates); + } + } + MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass); if (calledMethod != null) { Map nextArgsMap = buildArgsMap(calledMethod, mi, argsMap); @@ -186,7 +265,7 @@ public class StateMachineAggregator { } private Map buildArgsMap(MethodDeclaration method, MethodInvocation call, Map currentArgsMap) { - Map map = new HashMap<>(); + Map map = new HashMap<>(currentArgsMap); List params = method.parameters(); List args = call.arguments(); @@ -196,24 +275,41 @@ public class StateMachineAggregator { if (param.isVarargs() && i < params.size()) { List varargList = new ArrayList<>(); for (int j = i; j < args.size(); j++) { - varargList.add(resolveExpression((Expression) args.get(j), currentArgsMap)); + Object resolved = resolve( (Expression) args.get(j), currentArgsMap); + if (resolved instanceof List list) { + for (Object item : list) { + if (item instanceof Expression e) varargList.add(e); + } + } else if (resolved instanceof Expression e) { + varargList.add(e); + } } map.put(paramName, varargList); } else if (i < args.size()) { - map.put(paramName, resolveExpression((Expression) args.get(i), currentArgsMap)); + map.put(paramName, resolve((Expression) args.get(i), currentArgsMap)); } } return map; } - private Expression resolveExpression(Expression expr, Map argsMap) { + private Object resolve(Expression expr, Map argsMap) { + if (expr instanceof NullLiteral) return null; if (expr instanceof SimpleName sn) { Object resolved = argsMap.get(sn.getIdentifier()); - if (resolved instanceof Expression e) return e; + if (resolved instanceof Expression nextExpr && nextExpr != expr) { + return resolve(nextExpr, argsMap); + } + if (resolved != null) return resolved; } return expr; } + private Expression resolveExpression(Expression expr, Map argsMap) { + Object resolved = resolve(expr, argsMap); + if (resolved instanceof Expression e) return e; + return expr; + } + private Map convertToExpressionMap(Map argsMap) { Map map = new HashMap<>(); for (Map.Entry entry : argsMap.entrySet()) { @@ -222,10 +318,23 @@ public class StateMachineAggregator { return map; } - private boolean isTransitionChain(MethodInvocation mi) { + private boolean isTransitionChainEntry(MethodInvocation mi) { String name = mi.getName().getIdentifier(); if (Set.of("withExternal", "withInternal", "withLocal", "withFork", "withJoin", "withChoice", "withJunction").contains(name)) return true; - return mi.getExpression() instanceof MethodInvocation next && isTransitionChain(next); + Expression receiver = mi.getExpression(); + if (receiver instanceof MethodInvocation next) return isTransitionChainEntry(next); + return false; + } + + private boolean isTransitionChainContinuation(MethodInvocation mi, Map argsMap) { + String name = mi.getName().getIdentifier(); + if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) { + Expression receiver = mi.getExpression(); + if (receiver instanceof MethodInvocation next) return isTransitionChainContinuation(next, argsMap) || isTransitionChainEntry(next); + Object resolved = resolve(receiver, argsMap); + return resolved instanceof MethodInvocation || resolved == null; // null means it's a parameter we track + } + return false; } private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) { @@ -251,43 +360,55 @@ public class StateMachineAggregator { return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().startsWith("StateMachineTransitionConfigurer"); } - public void aggregateStates(TypeDeclaration startClass, Set initialStates, Set endStates) { + @Getter + private final Set initialStates = new HashSet<>(); + @Getter + private final Set endStates = new HashSet<>(); + + public void aggregateStates(TypeDeclaration startClass) { aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), new HashSet<>(), new HashMap<>()); } - private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedClasses, Set visitedMethods, Map argsMap) { + private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedClasses, Set visitedMethods, Map argsMap) { 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, argsMap); + // Hierarchy - Superclass Type superclassType = currentTd.getSuperclassType(); if (superclassType != null) { TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType)); if (superclassTd != null) aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap); } - } - private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedMethods, Map argsMap) { - if (visitedMethods.contains(method)) return; - visitedMethods.add(method); - Block body = method.getBody(); - if (body == null) return; - - for (Object stmt : body.statements()) { - if (stmt instanceof ExpressionStatement es) parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap); - else if (stmt instanceof VariableDeclarationStatement vds) { - for (Object fragmentObj : vds.fragments()) { - if (fragmentObj instanceof VariableDeclarationFragment fragment) parseStatesInExpression(fragment.getInitializer(), searchStartClass, initialStates, endStates, visitedMethods, argsMap); - } - } else if (stmt instanceof EnhancedForStatement efs) parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, false, initialStates, endStates); + // Hierarchy - Interfaces + for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) { + if (interfaceTypeObj instanceof Type interfaceType) { + TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType)); + if (interfaceTd != null) aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap); + } } } - private boolean isWithStatesChain(MethodInvocation mi) { + private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set initialStates, Set endStates, Set visitedMethods, Map argsMap) { + MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap)); + if (visitedMethods.contains(visit)) return; + visitedMethods.add(visit); + Block body = method.getBody(); + if (body == null) return; + + parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap); + } + + private boolean isWithStatesChain(MethodInvocation mi, Map argsMap) { if (mi.getName().getIdentifier().equals("withStates")) return true; - return mi.getExpression() instanceof MethodInvocation next && isWithStatesChain(next); + Expression receiver = mi.getExpression(); + if (receiver instanceof MethodInvocation next) return isWithStatesChain(next, argsMap); + Object resolved = resolve(receiver, argsMap); + if (resolved instanceof MethodInvocation next) return isWithStatesChain(next, argsMap); + return false; } private void parseStateChain(MethodInvocation mi, Set initialStates, Set endStates, Map argsMap) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/TransitionStateUtils.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/TransitionStateUtils.java index 9f1350f..5373867 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/TransitionStateUtils.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/TransitionStateUtils.java @@ -9,6 +9,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; public class TransitionStateUtils { + public static Set findStartStates(Collection transitions, Set initialStates) { + if (initialStates != null && !initialStates.isEmpty()) { + return initialStates; + } + return findStartStates(transitions); + } + public static Set findStartStates(Collection transitions) { Set allTargetStates = transitions.stream() .flatMap(t -> normalizeStates(t.getTargetStates())) @@ -19,6 +26,13 @@ public class TransitionStateUtils { .collect(Collectors.toSet()); } + public static Set findEndStates(Collection transitions, Set endStates) { + if (endStates != null && !endStates.isEmpty()) { + return endStates; + } + return findEndStates(transitions); + } + public static Set findEndStates(Collection transitions) { Set allSourceStates = transitions.stream() .flatMap(t -> normalizeStates(t.getSourceStates())) 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 index f19623e..78b8cf3 100644 --- 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 @@ -87,17 +87,27 @@ public class CodebaseContext { } public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) { - Set knownAdapters = Set.of("EnumStateMachineConfigurerAdapter", "StateMachineConfigurerAdapter"); + Set knownAdapters = Set.of("EnumStateMachineConfigurerAdapter", "StateMachineConfigurerAdapter", "StateMachineConfigurer"); + + // Check superclass 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); + if (superclass != null) { + String superclassName = getSimpleName(superclass); + if (knownAdapters.contains(superclassName)) return true; + TypeDeclaration superTd = getTypeDeclaration(superclassName); + if (superTd != null && extendsStateMachineConfigurerAdapter(superTd)) return true; } + + // Check interfaces + for (Object interfaceTypeObj : td.superInterfaceTypes()) { + if (interfaceTypeObj instanceof Type interfaceType) { + String interfaceName = getSimpleName(interfaceType); + if (knownAdapters.contains(interfaceName)) return true; + TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName); + if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd)) return true; + } + } + return false; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java index a6fcff3..06c2da9 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/out/PlantUml.java @@ -130,7 +130,7 @@ public class PlantUml implements StateMachineExporter { @Override public String getFileExtension() { - return ".plantuml.dot"; + return ".puml"; } private String getGuardText(Transition t, boolean useLambdaGuards) { 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 5eb9392..367a6b0 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 @@ -9,6 +9,7 @@ import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregatorTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregatorTest.java index df37bc6..8cc9424 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregatorTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/app/StateMachineAggregatorTest.java @@ -173,12 +173,10 @@ class StateMachineAggregatorTest { context.scan(tempDir); TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig"); - Set initialStates = new HashSet<>(); - Set endStates = new HashSet<>(); - aggregator.aggregateStates(childTd, initialStates, endStates); + aggregator.aggregateStates(childTd); - assertThat(initialStates).containsExactly("BASE_START"); - assertThat(endStates).containsExactly("CHILD_END"); + assertThat(aggregator.getInitialStates()).containsExactly("BASE_START"); + assertThat(aggregator.getEndStates()).containsExactly("CHILD_END"); } @Test @@ -199,11 +197,9 @@ class StateMachineAggregatorTest { context.scan(tempDir); TypeDeclaration td = context.getTypeDeclaration("StreamStateConfig"); - Set initialStates = new HashSet<>(); - Set endStates = new HashSet<>(); - aggregator.aggregateStates(td, initialStates, endStates); + aggregator.aggregateStates(td); - assertThat(initialStates).containsExactlyInAnyOrder("S1", "S2"); + assertThat(aggregator.getInitialStates()).containsExactlyInAnyOrder("S1", "S2"); } @Test diff --git a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml new file mode 100644 index 0000000..0ca724f --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml @@ -0,0 +1,59 @@ +@startuml +skinparam state { + BackgroundColor<> LightYellow +} + +[*] --> STATE1 + +state STATE17 <> +state STATE16 <> +state STATE14 <> +state STATE12 <> +state STATE11 <> +state STATE8 <> +state STATE20 <> +state STATE9 <> +state STATE19 <> +state STATE18 <> + +STATE1 -[#1E90FF,bold]-> STATE2 : EVENT1 +STATE2 -[#1E90FF,bold]-> STATE3 : EVENT2 +STATE3 -[#1E90FF,bold]-> STATE4 : EVENT3 +STATE4 -[#1E90FF,bold]-> STATE5 : EVENT4 +STATE5 -[#1E90FF,bold]-> STATE6 : EVENT5 +STATE6 -[#1E90FF,bold]-> STATE7 : EVENT6 +STATE7 -[#1E90FF,bold]-> STATE8 : EVENT7 +STATE8 -[#1E90FF,bold]-> STATE9 : EVENT8 +STATE9 -[#1E90FF,bold]-> STATE10 : EVENT9 +STATE10 -[#1E90FF,bold]-> STATE11 : EVENT10 +STATE11 -[#1E90FF,bold]-> STATE12 : EVENT11 +STATE12 -[#1E90FF,bold]-> STATE13 : EVENT12 +STATE13 -[#1E90FF,bold]-> STATE14 : EVENT13 +STATE14 -[#1E90FF,bold]-> STATE15 : EVENT14 +STATE15 -[#1E90FF,bold]-> STATE16 : EVENT15 +STATE16 -[#4682B4,bold]-> STATE17 : [guardVarEquals("value1")] (order=0) +STATE16 -[#4682B4,bold]-> STATE18 : [guardVarEquals("value2")] (order=1) +STATE16 -[#4682B4,bold]-> STATE19 : (order=2) +STATE17 -[#FF6347,bold]-> STATE20 : [guardEventHeaderEquals("header1","foo")] (order=0) +STATE17 -[#FF6347,bold]-> STATE16 : (order=1) +STATE18 -[#FFD700,bold]-> STATE19 : [guardVarEquals("value3")] (order=0) +STATE18 -[#FFD700,bold]-> STATE20 : (order=1) +STATE19 -[#32CD32,bold]-> STATE1 : [guardVarEquals("reset")] (order=0) +STATE19 -[#32CD32,bold]-> STATE20 : (order=1) +STATE20 -[#FF6347,bold]-> STATE5 : [guardEventHeaderEquals("header2","bar")] (order=0) +STATE20 -[#FF6347,bold]-> STATE1 : (order=1) +STATE11 -[#6A5ACD,bold]-> STATE13 : [guardVarEquals("goTo13")] (order=0) +STATE11 -[#6A5ACD,bold]-> STATE14 : [guardVarEquals("goTo14")] (order=1) +STATE11 -[#6A5ACD,bold]-> STATE15 : (order=2) +STATE12 -[#FFD700,bold]-> STATE10 : [guardVarEquals("goBack10")] (order=0) +STATE12 -[#FFD700,bold]-> STATE11 : (order=1) +STATE14 -[#32CD32,bold]-> STATE12 : [guardVarEquals("loop12")] (order=0) +STATE14 -[#32CD32,bold]-> STATE16 : (order=1) +STATE9 -[#4682B4,bold]-> STATE8 : [guardVarEquals("stepBack")] (order=0) +STATE9 -[#4682B4,bold]-> STATE7 : [guardVarEquals("stepBackMore")] (order=1) +STATE9 -[#4682B4,bold]-> STATE6 : (order=2) +STATE8 -[#FF69B4,bold]-> STATE9 : [guardVarEquals("forward9")] (order=0) +STATE8 -[#FF69B4,bold]-> STATE10 : (order=1) + +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/ForkJoinStateMachineConfig/ForkJoinStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ForkJoinStateMachineConfig/ForkJoinStateMachineConfig.puml new file mode 100644 index 0000000..3e957a5 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/ForkJoinStateMachineConfig/ForkJoinStateMachineConfig.puml @@ -0,0 +1,20 @@ +@startuml +skinparam state { + BackgroundColor<> LightYellow +} + +[*] --> START + + +START -[#1E90FF,bold]-> FORK : TO_FORK +FORK -[#20B2AA,bold]-> REGION1_STATE1 +FORK -[#20B2AA,bold]-> REGION2_STATE1 +REGION1_STATE1 -[#1E90FF,bold]-> REGION1_STATE2 : R1_NEXT +REGION2_STATE1 -[#1E90FF,bold]-> REGION2_STATE2 : R2_NEXT +REGION1_STATE2 -[#8A2BE2,bold]-> JOIN +REGION2_STATE2 -[#8A2BE2,bold]-> JOIN +JOIN -[#1E90FF,bold]-> END : TO_END + +END --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml new file mode 100644 index 0000000..a124ee4 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/KamilEnumStateMachineConfiguration/KamilEnumStateMachineConfiguration.puml @@ -0,0 +1,27 @@ +@startuml +skinparam state { + BackgroundColor<> LightYellow +} + +[*] --> SUBMITTED + +state PAID1 <> + +SUBMITTED -[#1E90FF,bold]-> PAID : PAY +PAID -[#1E90FF,bold]-> FULFILLED : FULFILL +SUBMITTED -[#1E90FF,bold]-> CANCELED : CANCEL [λ] +PAID -[#1E90FF,bold]-> CANCELED : CANCEL +SUBMITTED -[#1E90FF,bold]-> SUBMITTED : ABCD +SUBMITTED -[#1E90FF,bold]-> PAID1 : ABCD +PAID1 -[#FF6347,bold]-> PAID2 : [λ] (order=0) +PAID1 -[#FF6347,bold]-> PAID3 : [guard1] (order=1) +PAID1 -[#FF6347,bold]-> HAPPEN : [λ] (order=2) +PAID1 -[#FF6347,bold]-> CANCELED : (order=3) +PAID2 -[#1E90FF,bold]-> CANCELED +PAID3 -[#1E90FF,bold]-> CANCELED +PAID2 -[#1E90FF,bold]-> PAID2 : ABCD / λ, action2 + +CANCELED --> [*] +FULFILLED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/OneStateMachineConfiguration/OneStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/OneStateMachineConfiguration/OneStateMachineConfiguration.puml new file mode 100644 index 0000000..b36f6de --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OneStateMachineConfiguration/OneStateMachineConfiguration.puml @@ -0,0 +1,65 @@ +@startuml +skinparam state { + BackgroundColor<> LightYellow +} + +[*] --> STATE1 + +state STATE17 <> +state STATE16 <> +state STATE14 <> +state STATE12 <> +state STATE11 <> +state STATE8 <> +state STATEY <> +state STATE20 <> +state STATE9 <> +state STATE19 <> +state STATE18 <> + +STATE1 -[#1E90FF,bold]-> STATE2 : EVENT1 +STATE2 -[#1E90FF,bold]-> STATE3 : EVENT2 +STATE3 -[#1E90FF,bold]-> STATE4 : EVENT3 +STATE4 -[#1E90FF,bold]-> STATE5 : EVENT4 +STATE5 -[#1E90FF,bold]-> STATE6 : EVENT5 +STATE6 -[#1E90FF,bold]-> STATE7 : EVENT6 +STATE7 -[#1E90FF,bold]-> STATE8 : EVENT7 +STATE8 -[#1E90FF,bold]-> STATE9 : EVENT8 +STATE9 -[#1E90FF,bold]-> STATE10 : EVENT9 +STATE10 -[#1E90FF,bold]-> STATE11 : EVENT10 +STATE11 -[#1E90FF,bold]-> STATE12 : EVENT11 +STATE12 -[#1E90FF,bold]-> STATE13 : EVENT12 +STATE13 -[#1E90FF,bold]-> STATE14 : EVENT13 +STATE14 -[#1E90FF,bold]-> STATE15 : EVENT14 +STATE15 -[#1E90FF,bold]-> STATE16 : EVENT15 +STATE4 -[#1E90FF,bold]-> STATEY : EVENTY +STATEY -[#FF6347,bold]-> STATEX : [guardVarEquals("value1")] (order=0) +STATEY -[#FF6347,bold]-> STATEZ : (order=1) +STATE16 -[#4682B4,bold]-> STATE17 : [guardVarEquals("value1")] (order=0) +STATE16 -[#4682B4,bold]-> STATE18 : [guardVarEquals("value2")] (order=1) +STATE16 -[#4682B4,bold]-> STATE19 : (order=2) +STATE17 -[#FF6347,bold]-> STATE20 : [guardEventHeaderEquals("header1","foo")] (order=0) +STATE17 -[#FF6347,bold]-> STATE16 : (order=1) +STATE18 -[#6A5ACD,bold]-> STATE19 : [guardVarEquals("value3")] (order=0) +STATE18 -[#6A5ACD,bold]-> STATE20 : (order=1) +STATE19 -[#FFD700,bold]-> STATE1 : [guardVarEquals("reset")] (order=0) +STATE19 -[#FFD700,bold]-> STATE20 : (order=1) +STATE20 -[#4682B4,bold]-> STATE5 : [guardEventHeaderEquals("header2","bar")] (order=0) +STATE20 -[#4682B4,bold]-> STATE1 : (order=1) +STATE11 -[#6A5ACD,bold]-> STATE13 : [guardVarEquals("goTo13")] (order=0) +STATE11 -[#6A5ACD,bold]-> STATE14 : [guardVarEquals("goTo14")] (order=1) +STATE11 -[#6A5ACD,bold]-> STATE15 : (order=2) +STATE12 -[#FFD700,bold]-> STATE10 : [guardVarEquals("goBack10")] (order=0) +STATE12 -[#FFD700,bold]-> STATE11 : (order=1) +STATE14 -[#32CD32,bold]-> STATE12 : [guardVarEquals("loop12")] (order=0) +STATE14 -[#32CD32,bold]-> STATE16 : (order=1) +STATE9 -[#32CD32,bold]-> STATE8 : [guardVarEquals("stepBack")] (order=0) +STATE9 -[#32CD32,bold]-> STATE7 : [guardVarEquals("stepBackMore")] (order=1) +STATE9 -[#32CD32,bold]-> STATE6 : (order=2) +STATE8 -[#FF69B4,bold]-> STATE9 : [guardVarEquals("forward9")] (order=0) +STATE8 -[#FF69B4,bold]-> STATE10 : (order=1) +STATE5 -[#1E90FF,bold]-> STATE_EXTRA_1 : EVENTX + +STATEZ --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/SimpleEnumStateMachineConfiguration/SimpleEnumStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/SimpleEnumStateMachineConfiguration/SimpleEnumStateMachineConfiguration.puml new file mode 100644 index 0000000..e752d38 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/SimpleEnumStateMachineConfiguration/SimpleEnumStateMachineConfiguration.puml @@ -0,0 +1,29 @@ +@startuml +skinparam state { + BackgroundColor<> LightYellow +} + +[*] --> SUBMITTED + +state PAID <> +state SUBMITTED <> + +SUBMITTED -[#1E90FF,bold]-> PAID : PAY +PAID -[#1E90FF,bold]-> FULFILLED : FULFILL +SUBMITTED -[#1E90FF,bold]-> CANCELED : CANCEL [λ] +PAID -[#1E90FF,bold]-> CANCELED : CANCEL +SUBMITTED -[#1E90FF,bold]-> SUBMITTED : ABCD +SUBMITTED -[#4682B4,bold]-> PAID2 : [λ] (order=0) +SUBMITTED -[#4682B4,bold]-> PAID3 : (order=1) +PAID -[#FF6347,bold]-> PAID1 : [λ] (order=0) +PAID -[#FF6347,bold]-> PAID2 : [guard1] (order=1) +PAID -[#FF6347,bold]-> HAPPEN : [λ] (order=2) +PAID -[#FF6347,bold]-> PAID3 : (order=3) +PAID2 -[#1E90FF,bold]-> CANCELED +PAID3 -[#1E90FF,bold]-> CANCELED +PAID1 -[#1E90FF,bold]-> PAID1 : ABCD / λ, action2 + +CANCELED --> [*] +FULFILLED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/TwoStateMachineExtraFunctionsConfiguration/TwoStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/TwoStateMachineExtraFunctionsConfiguration/TwoStateMachineConfiguration.puml new file mode 100644 index 0000000..3746f41 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/TwoStateMachineExtraFunctionsConfiguration/TwoStateMachineConfiguration.puml @@ -0,0 +1,72 @@ +@startuml +skinparam state { + BackgroundColor<> LightYellow +} + +[*] --> STATE1 + +state STATE17 <> +state STATE16 <> +state STATE14 <> +state STATE12 <> +state STATE11 <> +state STATE8 <> +state STATEY <> +state STATE20 <> +state STATE9 <> +state STATE19 <> +state STATE18 <> + +STATE1 -[#1E90FF,bold]-> STATE2 : EVENT1 +STATE2 -[#1E90FF,bold]-> STATE3 : EVENT2 +STATE3 -[#1E90FF,bold]-> STATE4 : EVENT3 +STATE4 -[#1E90FF,bold]-> STATE5 : EVENT4 +STATE5 -[#1E90FF,bold]-> STATE6 : EVENT5 +STATE6 -[#1E90FF,bold]-> STATE7 : EVENT6 +STATE7 -[#1E90FF,bold]-> STATE8 : EVENT7 +STATE8 -[#1E90FF,bold]-> STATE9 : EVENT8 +STATE9 -[#1E90FF,bold]-> STATE10 : EVENT9 +STATE10 -[#1E90FF,bold]-> STATE11 : EVENT10 +STATE11 -[#1E90FF,bold]-> STATE12 : EVENT11 +STATE12 -[#1E90FF,bold]-> STATE13 : EVENT12 +STATE13 -[#1E90FF,bold]-> STATE14 : EVENT13 +STATE14 -[#1E90FF,bold]-> STATE15 : EVENT14 +STATE15 -[#1E90FF,bold]-> STATE16 : EVENT15 +STATE1 -[#1E90FF,bold]-> CANCEL : EVENT_CANCEL +STATE2 -[#1E90FF,bold]-> CANCEL : EVENT_CANCEL +STATE3 -[#1E90FF,bold]-> CANCEL : EVENT_CANCEL +STATE4 -[#1E90FF,bold]-> CANCEL : EVENT_CANCEL +STATE5 -[#1E90FF,bold]-> CANCEL : EVENT_CANCEL +STATE4 -[#1E90FF,bold]-> STATEY : EVENTY +STATEY -[#FF6347,bold]-> STATEX : [guardVarEquals("value1")] (order=0) +STATEY -[#FF6347,bold]-> STATEZ : (order=1) +STATE16 -[#4682B4,bold]-> STATE17 : [guardVarEquals("value1")] (order=0) +STATE16 -[#4682B4,bold]-> STATE18 : [guardVarEquals("value2")] (order=1) +STATE16 -[#4682B4,bold]-> STATE19 : (order=2) +STATE17 -[#FF6347,bold]-> STATE20 : [guardEventHeaderEquals("header1","foo")] (order=0) +STATE17 -[#FF6347,bold]-> STATE16 : (order=1) +STATE18 -[#6A5ACD,bold]-> STATE19 : [guardVarEquals("value3")] (order=0) +STATE18 -[#6A5ACD,bold]-> STATE20 : (order=1) +STATE19 -[#FFD700,bold]-> STATE1 : [guardVarEquals("reset")] (order=0) +STATE19 -[#FFD700,bold]-> STATE20 : (order=1) +STATE20 -[#4682B4,bold]-> STATE5 : [guardEventHeaderEquals("header2","bar")] (order=0) +STATE20 -[#4682B4,bold]-> STATE1 : (order=1) +STATE11 -[#6A5ACD,bold]-> STATE13 : [guardVarEquals("goTo13")] (order=0) +STATE11 -[#6A5ACD,bold]-> STATE14 : [guardVarEquals("goTo14")] (order=1) +STATE11 -[#6A5ACD,bold]-> STATE15 : (order=2) +STATE12 -[#FFD700,bold]-> STATE10 : [guardVarEquals("goBack10")] (order=0) +STATE12 -[#FFD700,bold]-> STATE11 : (order=1) +STATE14 -[#32CD32,bold]-> STATE12 : [guardVarEquals("loop12")] (order=0) +STATE14 -[#32CD32,bold]-> STATE16 : (order=1) +STATE9 -[#32CD32,bold]-> STATE8 : [guardVarEquals("stepBack")] (order=0) +STATE9 -[#32CD32,bold]-> STATE7 : [guardVarEquals("stepBackMore")] (order=1) +STATE9 -[#32CD32,bold]-> STATE6 : (order=2) +STATE8 -[#FF69B4,bold]-> STATE9 : [guardVarEquals("forward9")] (order=0) +STATE8 -[#FF69B4,bold]-> STATE10 : (order=1) +STATE5 -[#1E90FF,bold]-> STATE_EXTRA_1 : EVENTX +STATE_EXTRA_1 -[#1E90FF,bold]-> CANCEL : EVENT_CANCEL +STATE_EXTRA_1 -[#1E90FF,bold]-> CANCEL : EVENT_CANCEL_2 + +STATEZ --> [*] +@enduml +