From 968601eefcd295f573df7a9228d4e415490210bb Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 21 Jun 2026 12:19:15 +0200 Subject: [PATCH] event maching heuristic update --- .../HeuristicEventMatchingEngine.java | 5 +- .../resolver/PolymorphicEventResolver.java | 22 + .../service/GenericEventDetector.java | 4 + .../service/HeuristicCallGraphEngine.java | 165 +++-- .../analysis/service/JdtCallGraphEngine.java | 164 +++-- .../RegressionTest.java | 6 + .../service/HeuristicCallGraphEngineTest.java | 57 ++ .../PolymorphicStateMachineConfiguration.dot | 15 + .../PolymorphicStateMachineConfiguration.json | 639 ++++++++++++++++++ .../PolymorphicStateMachineConfiguration.puml | 35 + ...morphicStateMachineConfiguration.scxml.xml | 16 + .../polymorphic_events_sample/.gitignore | 37 + .../polymorphic_events_sample/build.gradle | 45 ++ .../statemachine/polymorphic/AbcdEvent.java | 8 + .../statemachine/polymorphic/BaseEvent.java | 2 + .../statemachine/polymorphic/CancelEvent.java | 4 + .../polymorphic/CustomCodeEventInterface.java | 5 + .../polymorphic/EventBuilder.java | 11 + .../polymorphic/FulfillEvent.java | 4 + .../polymorphic/MysteryPayload.java | 9 + .../statemachine/polymorphic/OrderEvents.java | 2 + .../polymorphic/OrderService.java | 23 + .../statemachine/polymorphic/OrderStates.java | 2 + .../statemachine/polymorphic/PayEvent.java | 4 + .../polymorphic/PolymorphicController.java | 79 +++ .../PolymorphicStateMachineConfiguration.java | 23 + .../app/StateMachineApplication.java | 7 + .../src/main/resources/application.properties | 1 + 28 files changed, 1317 insertions(+), 77 deletions(-) create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PolymorphicEventResolver.java create mode 100644 state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.dot create mode 100644 state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json create mode 100644 state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.puml create mode 100644 state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.scxml.xml create mode 100644 state_machines/polymorphic_events_sample/.gitignore create mode 100644 state_machines/polymorphic_events_sample/build.gradle create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/AbcdEvent.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/BaseEvent.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CancelEvent.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CustomCodeEventInterface.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/EventBuilder.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/FulfillEvent.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/MysteryPayload.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderEvents.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderStates.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PayEvent.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicStateMachineConfiguration.java create mode 100644 state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/app/StateMachineApplication.java create mode 100644 state_machines/polymorphic_events_sample/src/main/resources/application.properties diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java index 4afbc5c..fcfe4d3 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java @@ -26,7 +26,10 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { if (pe.contains(".")) { simplePe = pe.substring(pe.lastIndexOf('.') + 1); } - if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) { + String simplifiedPe = simplify(simplePe); + + if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) || + simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) { hasPolyMatch = true; break; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PolymorphicEventResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PolymorphicEventResolver.java new file mode 100644 index 0000000..151050a --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PolymorphicEventResolver.java @@ -0,0 +1,22 @@ +package click.kamil.springstatemachineexporter.analysis.resolver; + +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; + +import java.util.List; + +public interface PolymorphicEventResolver { + + /** + * Attempts to resolve a dynamically passed event variable into a set of + * concrete polymorphic event names. + * + * @param triggerPoint The original trigger point to resolve. + * @param resolvedValue The current String expression (e.g., "event.getType()" or "event") + * @param path The call path trace leading up to this variable. + * @param context The codebase context for deep type resolution. + * @return The updated TriggerPoint with polymorphic events set, or the original if unable to resolve. + */ + TriggerPoint resolvePolymorphicEvents(TriggerPoint triggerPoint, String resolvedValue, List path, CodebaseContext context); + +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java index ee78606..d7c1742 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java @@ -325,6 +325,10 @@ public class GenericEventDetector { } } + if (expr instanceof ClassInstanceCreation cic) { + return cic.getType().toString(); + } + if (!(expr instanceof MethodInvocation mi)) return null; // Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build() diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index 59f9a00..f504f5b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -142,43 +142,85 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } List polymorphicEvents = new ArrayList<>(); - if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) { - int lastDot = resolvedValue.lastIndexOf('.'); - int firstDot = resolvedValue.indexOf('.'); - int openParen = resolvedValue.indexOf('(', lastDot); - - if (lastDot > 0 && openParen > lastDot) { - String varName = resolvedValue.substring(0, firstDot); - if (varName.contains("(")) { - varName = null; + + // Parse resolvedValue using JDT to robustly handle complex expressions + org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.getJLSLatest()); + exprParser.setSource(resolvedValue.toCharArray()); + exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); + org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null); + + String varName = null; + String methodName = null; + String declaredType = null; + String sourceMethod = null; + + if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { + methodName = mi.getName().getIdentifier(); + if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + varName = sn.getIdentifier(); + } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe && + pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce && + ce.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + varName = sn.getIdentifier(); + declaredType = ce.getType().toString(); + sourceMethod = "inline-cast"; + } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + } else { + // Fallback for complex chained expressions + String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : ""; + if (!exprStr.contains("(")) { + varName = exprStr; } - String methodName = resolvedValue.substring(lastDot + 1, openParen); - - String declaredType = null; - String sourceMethod = null; - - if (varName != null) { - for (String methodFqn : path) { - declaredType = getVariableDeclaredType(methodFqn, varName); - if (declaredType != null) { - sourceMethod = methodFqn; - break; - } - } - } else { - String baseExpr = resolvedValue.substring(0, lastDot); - if (baseExpr.contains("new ")) { - int newIdx = baseExpr.indexOf("new "); - int openParenBase = baseExpr.indexOf('(', newIdx); - if (openParenBase > newIdx) { - declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim(); - if (declaredType.contains("<")) { - declaredType = declaredType.substring(0, declaredType.indexOf('<')); - } - sourceMethod = "inline-instantiation"; - } + } + } else if (exprNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + polymorphicEvents.add(declaredType); // Track the payload type as the event + } else if (exprNode instanceof org.eclipse.jdt.core.dom.ConditionalExpression cond) { + if (cond.getThenExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicThen) { + polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicThen.getType())); + } + if (cond.getElseExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicElse) { + polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicElse.getType())); + } + sourceMethod = "inline-ternary"; + declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0); + } else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + varName = sn.getIdentifier(); + methodName = "VariableReference"; // We just want to trigger deep trace + } else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe && + pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce && + ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + polymorphicEvents.add(declaredType); + } else if (exprNode instanceof org.eclipse.jdt.core.dom.CastExpression ce && + ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + polymorphicEvents.add(declaredType); + } + + if (methodName != null) { + if (varName != null && declaredType == null) { + for (String methodFqn : path) { + declaredType = getVariableDeclaredType(methodFqn, varName); + if (declaredType != null) { + sourceMethod = methodFqn; + break; } } + // If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent()) + if (declaredType == null && varName.matches("^[A-Z].*")) { + org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName); + if (staticTd != null) { + declaredType = context.getFqn(staticTd); + sourceMethod = "static-call"; + } + } + } if (declaredType != null) { System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType); @@ -215,9 +257,6 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } } } - } - - // LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant // (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly // for string literals or enum-like constants. boolean hasValidConstant = false; @@ -259,7 +298,16 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { return !val.equals(val.toUpperCase()) || val.length() <= 2; }); } - + List newPolyEvents = new ArrayList<>(); + for (String pe : polymorphicEvents) { + List resolved = resolveClassConstantReturns(pe, context, null); + if (resolved != null && !resolved.isEmpty()) { + newPolyEvents.addAll(resolved); + } else { + newPolyEvents.add(pe); + } + } + polymorphicEvents = newPolyEvents; if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { return TriggerPoint.builder() @@ -384,6 +432,10 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } else { constants.add(val); } + } else if (retExpr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) { + extractConstantsFromExpression(ce, constants); + } else if (retExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + constants.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType())); } else if (retExpr instanceof QualifiedName qn) { constants.add(qn.toString()); } else if (retExpr instanceof SimpleName sn) { @@ -405,6 +457,43 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { return constants; } + private List resolveClassConstantReturns(String className, click.kamil.springstatemachineexporter.ast.common.CodebaseContext context, CompilationUnit contextCu) { + TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className); + if (td == null) td = context.getTypeDeclaration(className); + if (td == null) return null; + + final List resolvedConstants = new ArrayList<>(); + for (MethodDeclaration md : td.getMethods()) { + if (md.getBody() != null) { + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + extractConstantsFromExpression(node.getExpression(), resolvedConstants); + return super.visit(node); + } + }); + } + if (!resolvedConstants.isEmpty()) return resolvedConstants; + } + return null; + } + + private void extractConstantsFromExpression(org.eclipse.jdt.core.dom.Expression expr, List constants) { + System.out.println("EXTRACT CONST: " + (expr != null ? expr.getClass().getSimpleName() + " " + expr.toString() : "null")); + if (expr instanceof QualifiedName qn) { + constants.add(qn.toString()); + } else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) { + constants.add(sl.getLiteralValue()); + } else if (expr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) { + extractConstantsFromExpression(ce.getThenExpression(), constants); + extractConstantsFromExpression(ce.getElseExpression(), constants); + } else if (expr instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe) { + extractConstantsFromExpression(pe.getExpression(), constants); + } else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) { + extractConstantsFromExpression(ce.getExpression(), constants); + } + } + private String traceLocalVariable(String methodFqn, String varName) { if (methodFqn == null || !methodFqn.contains(".")) return null; String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index e6e82f5..117d7c0 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -146,43 +146,85 @@ public class JdtCallGraphEngine implements CallGraphEngine { } List polymorphicEvents = new ArrayList<>(); - if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) { - int lastDot = resolvedValue.lastIndexOf('.'); - int firstDot = resolvedValue.indexOf('.'); - int openParen = resolvedValue.indexOf('(', lastDot); - - if (lastDot > 0 && openParen > lastDot) { - String varName = resolvedValue.substring(0, firstDot); - if (varName.contains("(")) { - varName = null; + + // Parse resolvedValue using JDT to robustly handle complex expressions + org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.getJLSLatest()); + exprParser.setSource(resolvedValue.toCharArray()); + exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); + org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null); + + String varName = null; + String methodName = null; + String declaredType = null; + String sourceMethod = null; + + if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { + methodName = mi.getName().getIdentifier(); + if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + varName = sn.getIdentifier(); + } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe && + pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce && + ce.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + varName = sn.getIdentifier(); + declaredType = ce.getType().toString(); + sourceMethod = "inline-cast"; + } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + } else { + // Fallback for complex chained expressions + String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : ""; + if (!exprStr.contains("(")) { + varName = exprStr; } - String methodName = resolvedValue.substring(lastDot + 1, openParen); - - String declaredType = null; - String sourceMethod = null; - - if (varName != null) { - for (String methodFqn : path) { - declaredType = getVariableDeclaredType(methodFqn, varName); - if (declaredType != null) { - sourceMethod = methodFqn; - break; - } - } - } else { - String baseExpr = resolvedValue.substring(0, lastDot); - if (baseExpr.contains("new ")) { - int newIdx = baseExpr.indexOf("new "); - int openParenBase = baseExpr.indexOf('(', newIdx); - if (openParenBase > newIdx) { - declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim(); - if (declaredType.contains("<")) { - declaredType = declaredType.substring(0, declaredType.indexOf('<')); - } - sourceMethod = "inline-instantiation"; - } + } + } else if (exprNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + polymorphicEvents.add(declaredType); // Track the payload type as the event + } else if (exprNode instanceof org.eclipse.jdt.core.dom.ConditionalExpression cond) { + if (cond.getThenExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicThen) { + polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicThen.getType())); + } + if (cond.getElseExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicElse) { + polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicElse.getType())); + } + sourceMethod = "inline-ternary"; + declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0); + } else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + varName = sn.getIdentifier(); + methodName = "VariableReference"; // We just want to trigger deep trace + } else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe && + pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce && + ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + polymorphicEvents.add(declaredType); + } else if (exprNode instanceof org.eclipse.jdt.core.dom.CastExpression ce && + ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + sourceMethod = "inline-instantiation"; + polymorphicEvents.add(declaredType); + } + + if (methodName != null) { + if (varName != null && declaredType == null) { + for (String methodFqn : path) { + declaredType = getVariableDeclaredType(methodFqn, varName); + if (declaredType != null) { + sourceMethod = methodFqn; + break; } } + // If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent()) + if (declaredType == null && varName.matches("^[A-Z].*")) { + org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName); + if (staticTd != null) { + declaredType = context.getFqn(staticTd); + sourceMethod = "static-call"; + } + } + } if (declaredType != null) { System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType); @@ -219,9 +261,6 @@ public class JdtCallGraphEngine implements CallGraphEngine { } } } - } - - // LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant // (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly // for string literals or enum-like constants. boolean hasValidConstant = false; @@ -263,7 +302,16 @@ public class JdtCallGraphEngine implements CallGraphEngine { return !val.equals(val.toUpperCase()) || val.length() <= 2; }); } - + List newPolyEvents = new ArrayList<>(); + for (String pe : polymorphicEvents) { + List resolved = resolveClassConstantReturns(pe, context, null); + if (resolved != null && !resolved.isEmpty()) { + newPolyEvents.addAll(resolved); + } else { + newPolyEvents.add(pe); + } + } + polymorphicEvents = newPolyEvents; if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { return TriggerPoint.builder() @@ -388,6 +436,10 @@ public class JdtCallGraphEngine implements CallGraphEngine { } else { constants.add(val); } + } else if (retExpr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) { + extractConstantsFromExpression(ce, constants); + } else if (retExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + constants.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType())); } else if (retExpr instanceof QualifiedName qn) { constants.add(qn.toString()); } else if (retExpr instanceof SimpleName sn) { @@ -409,6 +461,42 @@ public class JdtCallGraphEngine implements CallGraphEngine { return constants; } + private List resolveClassConstantReturns(String className, click.kamil.springstatemachineexporter.ast.common.CodebaseContext context, CompilationUnit contextCu) { + TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className); + if (td == null) td = context.getTypeDeclaration(className); + if (td == null) return null; + + final List resolvedConstants = new ArrayList<>(); + for (MethodDeclaration md : td.getMethods()) { + if (md.getBody() != null) { + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + extractConstantsFromExpression(node.getExpression(), resolvedConstants); + return super.visit(node); + } + }); + } + if (!resolvedConstants.isEmpty()) return resolvedConstants; + } + return null; + } + + private void extractConstantsFromExpression(org.eclipse.jdt.core.dom.Expression expr, List constants) { + if (expr instanceof QualifiedName qn) { + constants.add(qn.toString()); + } else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) { + constants.add(sl.getLiteralValue()); + } else if (expr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) { + extractConstantsFromExpression(ce.getThenExpression(), constants); + extractConstantsFromExpression(ce.getElseExpression(), constants); + } else if (expr instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe) { + extractConstantsFromExpression(pe.getExpression(), constants); + } else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) { + extractConstantsFromExpression(ce.getExpression(), constants); + } + } + private String traceLocalVariable(String methodFqn, String varName) { if (methodFqn == null || !methodFqn.contains(".")) return null; String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); 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 c6c1fbb..c5b5e84 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 @@ -143,6 +143,12 @@ public class RegressionTest { root.resolve("state_machines/complex_multi_module_sm"), Path.of("src/test/resources/golden/StateMachineConfig"), "StateMachineConfig" + ), + new TestScenario( + "Polymorphic Events Sample", + root.resolve("state_machines/polymorphic_events_sample"), + Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"), + "PolymorphicStateMachineConfiguration" ) ); } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java index 5d32cc6..0909b95 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java @@ -292,6 +292,63 @@ class HeuristicCallGraphEngineTest { .containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED"); } + @Test + void shouldResolveTernaryAndStringPolymorphicReturns() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + service.updateOrderState(new MysteryPayload().getType()); + } + } + + class OrderService { + public void updateOrderState(Object event) { + // sendEvent + } + } + + class MysteryPayload { + public Object getType() { + int a = 5; + if (a > 10) { + return "RAW_STRING_EVENT"; + } + return a > 5 ? OrderEvents.ABCD : OrderEvents.PAY; + } + } + + enum OrderEvents { ABCD, PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_ternary_string"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .as("Resolved event was: " + chain.getTriggerPoint().getEvent()) + .containsExactlyInAnyOrder("OrderEvents.ABCD", "OrderEvents.PAY", "RAW_STRING_EVENT"); + } + @Test void shouldUnwrapDeepMethodWrappers() throws IOException { String source = """ diff --git a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.dot b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.dot new file mode 100644 index 0000000..4617a45 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.dot @@ -0,0 +1,15 @@ +digraph statemachine { + rankdir=LR; + node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; + edge [fontname="Arial", fontsize=10]; + + _start [shape=circle, label="", fillcolor=black, width=0.1]; + _start -> SUBMITTED; + CANCELED [fillcolor=lightgray]; + FULFILLED [fillcolor=lightgray]; + SUBMITTED -> PAID [label="OrderEvents.PAY", style="solid", color="black"]; + PAID -> FULFILLED [label="OrderEvents.FULFILL", style="solid", color="black"]; + SUBMITTED -> CANCELED [label="OrderEvents.CANCEL", style="solid", color="black"]; + PAID -> CANCELED [label="OrderEvents.ABCD", style="solid", color="black"]; +} + diff --git a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json new file mode 100644 index 0000000..c13a12c --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json @@ -0,0 +1,639 @@ +{ + "metadata" : { + "triggers" : [ { + "event" : "event", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : null + }, { + "event" : "payload", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processPayloadEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 17, + "polymorphicEvents" : null + }, { + "event" : "event", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processCustomEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 21, + "polymorphicEvents" : null + } ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /pay", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "pay", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /fulfill", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "fulfill", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/fulfill", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /cancel", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "cancel", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/cancel", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /payload-pay", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payloadPay", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/payload-pay", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /pay-variable", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payVariable", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-variable", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /pay-cast", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payCast", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-cast", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /pay-ternary", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payTernary", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-ternary", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "isPay", + "type" : "boolean", + "annotations" : [ ] + } ] + }, { + "type" : "REST", + "name" : "POST /pay-list", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payList", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-list", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /pay-builder-static", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payBuilderStatic", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-builder-static", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /pay-builder-instance", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payBuilderInstance", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-builder-instance", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /abcd", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "abcd", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/abcd", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /mystery", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "mystery", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/mystery", + "verb" : "POST" + }, + "parameters" : [ ] + } ], + "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /pay", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "pay", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.pay", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "new PayEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ "OrderEvents.PAY" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.PAID", + "event" : "OrderEvents.PAY" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /fulfill", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "fulfill", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/fulfill", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.fulfill", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "new FulfillEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ "OrderEvents.FULFILL" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.PAID", + "targetState" : "OrderStates.FULFILLED", + "event" : "OrderEvents.FULFILL" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /cancel", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "cancel", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/cancel", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.cancel", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "new CancelEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ "OrderEvents.CANCEL" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.CANCEL" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /payload-pay", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payloadPay", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/payload-pay", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payloadPay", "click.kamil.examples.statemachine.polymorphic.OrderService.processPayloadEvent" ], + "triggerPoint" : { + "event" : "new PayEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processPayloadEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 17, + "polymorphicEvents" : [ "OrderEvents.PAY" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.PAID", + "event" : "OrderEvents.PAY" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /pay-variable", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payVariable", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-variable", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payVariable", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "new PayEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ "OrderEvents.PAY" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.PAID", + "event" : "OrderEvents.PAY" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /pay-cast", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payCast", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-cast", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payCast", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "(BaseEvent)new PayEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ "OrderEvents.PAY" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.PAID", + "event" : "OrderEvents.PAY" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /pay-ternary", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payTernary", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-ternary", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "isPay", + "type" : "boolean", + "annotations" : [ ] + } ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payTernary", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "isPay ? new PayEvent() : new CancelEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ ] + }, + "contextMachineId" : null, + "matchedTransitions" : null + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /pay-list", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payList", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-list", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "event", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : null + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.PAID", + "event" : "OrderEvents.PAY" + }, { + "sourceState" : "OrderStates.PAID", + "targetState" : "OrderStates.FULFILLED", + "event" : "OrderEvents.FULFILL" + }, { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.CANCEL" + }, { + "sourceState" : "OrderStates.PAID", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.ABCD" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /pay-builder-static", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payBuilderStatic", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-builder-static", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "EventBuilder.buildEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ "OrderEvents.FULFILL" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.PAID", + "targetState" : "OrderStates.FULFILLED", + "event" : "OrderEvents.FULFILL" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /pay-builder-instance", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "payBuilderInstance", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/pay-builder-instance", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], + "triggerPoint" : { + "event" : "new EventBuilder().buildInstanceEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 13, + "polymorphicEvents" : [ "OrderEvents.CANCEL" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.CANCEL" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /abcd", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "abcd", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/abcd", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.abcd", "click.kamil.examples.statemachine.polymorphic.OrderService.processCustomEvent" ], + "triggerPoint" : { + "event" : "new AbcdEvent()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processCustomEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 21, + "polymorphicEvents" : [ "OrderEvents.ABCD" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.PAID", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.ABCD" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /mystery", + "className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController", + "methodName" : "mystery", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java", + "metadata" : { + "path" : "/mystery", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.mystery", "click.kamil.examples.statemachine.polymorphic.OrderService.processCustomEvent" ], + "triggerPoint" : { + "event" : "new MysteryPayload()", + "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", + "methodName" : "processCustomEvent", + "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 21, + "polymorphicEvents" : [ "OrderEvents.ABCD", "OrderEvents.PAY" ] + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.PAID", + "event" : "OrderEvents.PAY" + }, { + "sourceState" : "OrderStates.PAID", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.ABCD" + } ] + } ], + "properties" : { + "default" : { + "spring.application.name" : "statemachinedemo" + } + } + }, + "name" : "click.kamil.examples.statemachine.polymorphic.PolymorphicStateMachineConfiguration", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "OrderStates.SUBMITTED" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderStates.SUBMITTED", + "fullIdentifier" : "OrderStates.SUBMITTED" + } ], + "targetStates" : [ { + "rawName" : "OrderStates.PAID", + "fullIdentifier" : "OrderStates.PAID" + } ], + "event" : { + "rawName" : "OrderEvents.PAY", + "fullIdentifier" : "OrderEvents.PAY" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderStates.PAID", + "fullIdentifier" : "OrderStates.PAID" + } ], + "targetStates" : [ { + "rawName" : "OrderStates.FULFILLED", + "fullIdentifier" : "OrderStates.FULFILLED" + } ], + "event" : { + "rawName" : "OrderEvents.FULFILL", + "fullIdentifier" : "OrderEvents.FULFILL" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderStates.SUBMITTED", + "fullIdentifier" : "OrderStates.SUBMITTED" + } ], + "targetStates" : [ { + "rawName" : "OrderStates.CANCELED", + "fullIdentifier" : "OrderStates.CANCELED" + } ], + "event" : { + "rawName" : "OrderEvents.CANCEL", + "fullIdentifier" : "OrderEvents.CANCEL" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderStates.PAID", + "fullIdentifier" : "OrderStates.PAID" + } ], + "targetStates" : [ { + "rawName" : "OrderStates.CANCELED", + "fullIdentifier" : "OrderStates.CANCELED" + } ], + "event" : { + "rawName" : "OrderEvents.ABCD", + "fullIdentifier" : "OrderEvents.ABCD" + }, + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.puml new file mode 100644 index 0000000..8fae88b --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.puml @@ -0,0 +1,35 @@ +@startuml +!pragma layout smetana +set separator none +hide empty description +hide stereotype +skinparam state { + BackgroundColor white + BorderColor #94a3b8 + BorderThickness 1 + FontName Inter + FontSize 9 + FontStyle bold + RoundCorner 20 + Padding 1 +} +skinparam shadowing false +skinparam ArrowFontName JetBrains Mono +skinparam ArrowFontSize 8 +skinparam ArrowColor #cbd5e1 +skinparam ArrowThickness 1 +skinparam dpi 110 +skinparam svgLinkTarget _self + +[*] --> OrderStates.SUBMITTED + + +OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID <> : OrderEvents.PAY +OrderStates.PAID -[#1E90FF,bold]-> OrderStates.FULFILLED <> : OrderEvents.FULFILL +OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.CANCELED <> : OrderEvents.CANCEL +OrderStates.PAID -[#1E90FF,bold]-> OrderStates.CANCELED <> : OrderEvents.ABCD + +OrderStates.CANCELED --> [*] +OrderStates.FULFILLED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.scxml.xml b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.scxml.xml new file mode 100644 index 0000000..1e13798 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.scxml.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/state_machines/polymorphic_events_sample/.gitignore b/state_machines/polymorphic_events_sample/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/state_machines/polymorphic_events_sample/.gitignore @@ -0,0 +1,37 @@ +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/ diff --git a/state_machines/polymorphic_events_sample/build.gradle b/state_machines/polymorphic_events_sample/build.gradle new file mode 100644 index 0000000..d2567bf --- /dev/null +++ b/state_machines/polymorphic_events_sample/build.gradle @@ -0,0 +1,45 @@ +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() +} + +bootJar { enabled = false } +jar { enabled = true } diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/AbcdEvent.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/AbcdEvent.java new file mode 100644 index 0000000..7fd91b1 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/AbcdEvent.java @@ -0,0 +1,8 @@ +package click.kamil.examples.statemachine.polymorphic; + +public class AbcdEvent implements CustomCodeEventInterface { + @Override + public OrderEvents resolveEventCode() { + return OrderEvents.ABCD; + } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/BaseEvent.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/BaseEvent.java new file mode 100644 index 0000000..890d61f --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/BaseEvent.java @@ -0,0 +1,2 @@ +package click.kamil.examples.statemachine.polymorphic; +public interface BaseEvent { OrderEvents getType(); } diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CancelEvent.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CancelEvent.java new file mode 100644 index 0000000..0cf4a58 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CancelEvent.java @@ -0,0 +1,4 @@ +package click.kamil.examples.statemachine.polymorphic; +public class CancelEvent implements BaseEvent { + @Override public OrderEvents getType() { return OrderEvents.CANCEL; } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CustomCodeEventInterface.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CustomCodeEventInterface.java new file mode 100644 index 0000000..39febe2 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/CustomCodeEventInterface.java @@ -0,0 +1,5 @@ +package click.kamil.examples.statemachine.polymorphic; + +public interface CustomCodeEventInterface { + OrderEvents resolveEventCode(); +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/EventBuilder.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/EventBuilder.java new file mode 100644 index 0000000..c319796 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/EventBuilder.java @@ -0,0 +1,11 @@ +package click.kamil.examples.statemachine.polymorphic; + +public class EventBuilder { + public static BaseEvent buildEvent() { + return new FulfillEvent(); + } + + public BaseEvent buildInstanceEvent() { + return new CancelEvent(); + } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/FulfillEvent.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/FulfillEvent.java new file mode 100644 index 0000000..63876a8 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/FulfillEvent.java @@ -0,0 +1,4 @@ +package click.kamil.examples.statemachine.polymorphic; +public class FulfillEvent implements BaseEvent { + @Override public OrderEvents getType() { return OrderEvents.FULFILL; } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/MysteryPayload.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/MysteryPayload.java new file mode 100644 index 0000000..509bd1e --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/MysteryPayload.java @@ -0,0 +1,9 @@ +package click.kamil.examples.statemachine.polymorphic; + +public class MysteryPayload implements CustomCodeEventInterface { + @Override + public OrderEvents resolveEventCode() { + int a = (int) (Math.random() * 10); + return a > 5 ? OrderEvents.ABCD : OrderEvents.PAY; + } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderEvents.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderEvents.java new file mode 100644 index 0000000..92a9ee0 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderEvents.java @@ -0,0 +1,2 @@ +package click.kamil.examples.statemachine.polymorphic; +public enum OrderEvents { FULFILL, PAY, CANCEL, ABCD, NOPE, IGNORE } diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java new file mode 100644 index 0000000..6a4413b --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java @@ -0,0 +1,23 @@ +package click.kamil.examples.statemachine.polymorphic; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Service; +import org.springframework.messaging.support.MessageBuilder; + +@Service +public class OrderService { + private final StateMachine sm; + + public OrderService(StateMachine sm) { this.sm = sm; } + + public void processEvent(BaseEvent event) { + sm.sendEvent(event.getType()); + } + + public void processPayloadEvent(Object payload) { + sm.sendEvent(MessageBuilder.withPayload(payload).build()); + } + + public void processCustomEvent(CustomCodeEventInterface event) { + sm.sendEvent(event.resolveEventCode()); + } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderStates.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderStates.java new file mode 100644 index 0000000..754760f --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/OrderStates.java @@ -0,0 +1,2 @@ +package click.kamil.examples.statemachine.polymorphic; +public enum OrderStates { SUBMITTED, PAID, FULFILLED, CANCELED, PAID1, PAID2, PAID3, INVALID, HAPPEN, SKIPPED, NEVER } diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PayEvent.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PayEvent.java new file mode 100644 index 0000000..4444963 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PayEvent.java @@ -0,0 +1,4 @@ +package click.kamil.examples.statemachine.polymorphic; +public class PayEvent implements BaseEvent { + @Override public OrderEvents getType() { return OrderEvents.PAY; } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java new file mode 100644 index 0000000..3ac2a43 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java @@ -0,0 +1,79 @@ +package click.kamil.examples.statemachine.polymorphic; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PolymorphicController { + + private final OrderService orderService; + + public PolymorphicController(OrderService orderService) { + this.orderService = orderService; + } + + @PostMapping("/pay") + public void pay() { + orderService.processEvent(new PayEvent()); + } + + @PostMapping("/fulfill") + public void fulfill() { + orderService.processEvent(new FulfillEvent()); + } + + @PostMapping("/cancel") + public void cancel() { + orderService.processEvent(new CancelEvent()); + } + + @PostMapping("/payload-pay") + public void payloadPay() { + orderService.processPayloadEvent(new PayEvent()); + } + + @PostMapping("/pay-variable") + public void payVariable() { + PayEvent event = new PayEvent(); + orderService.processEvent(event); + } + + @PostMapping("/pay-cast") + public void payCast() { + orderService.processEvent((BaseEvent) new PayEvent()); + } + + @PostMapping("/pay-ternary") + public void payTernary(boolean isPay) { + orderService.processEvent(isPay ? new PayEvent() : new CancelEvent()); + } + + @PostMapping("/pay-list") + public void payList() { + java.util.List events = java.util.Arrays.asList(new PayEvent(), new FulfillEvent()); + for (BaseEvent event : events) { + orderService.processEvent(event); + } + } + + @PostMapping("/pay-builder-static") + public void payBuilderStatic() { + orderService.processEvent(EventBuilder.buildEvent()); + } + + @PostMapping("/pay-builder-instance") + public void payBuilderInstance() { + EventBuilder builder = new EventBuilder(); + orderService.processEvent(builder.buildInstanceEvent()); + } + + @PostMapping("/abcd") + public void abcd() { + orderService.processCustomEvent(new AbcdEvent()); + } + + @PostMapping("/mystery") + public void mystery() { + orderService.processCustomEvent(new MysteryPayload()); + } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicStateMachineConfiguration.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicStateMachineConfiguration.java new file mode 100644 index 0000000..e9892d9 --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicStateMachineConfiguration.java @@ -0,0 +1,23 @@ +package click.kamil.examples.statemachine.polymorphic; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +@Configuration +@EnableStateMachine +public class PolymorphicStateMachineConfiguration extends EnumStateMachineConfigurerAdapter { + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states.withStates().initial(OrderStates.SUBMITTED).state(OrderStates.PAID).state(OrderStates.FULFILLED).state(OrderStates.CANCELED); + } + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY).and() + .withExternal().source(OrderStates.PAID).target(OrderStates.FULFILLED).event(OrderEvents.FULFILL).and() + .withExternal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELED).event(OrderEvents.CANCEL).and() + .withExternal().source(OrderStates.PAID).target(OrderStates.CANCELED).event(OrderEvents.ABCD); + } +} diff --git a/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/app/StateMachineApplication.java b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/app/StateMachineApplication.java new file mode 100644 index 0000000..94462aa --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/java/click/kamil/examples/statemachine/polymorphic/app/StateMachineApplication.java @@ -0,0 +1,7 @@ +package click.kamil.examples.statemachine.polymorphic.app; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +@SpringBootApplication +public class StateMachineApplication { + public static void main(String[] args) { SpringApplication.run(StateMachineApplication.class, args); } +} diff --git a/state_machines/polymorphic_events_sample/src/main/resources/application.properties b/state_machines/polymorphic_events_sample/src/main/resources/application.properties new file mode 100644 index 0000000..1cd01ff --- /dev/null +++ b/state_machines/polymorphic_events_sample/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=statemachinedemo