diff --git a/out_complex_multi_module_sm/click.kamil.domain.StateMachineConfig/click.kamil.domain.StateMachineConfig.html b/out_complex_multi_module_sm/click.kamil.domain.StateMachineConfig/click.kamil.domain.StateMachineConfig.html new file mode 100644 index 0000000..f6408af --- /dev/null +++ b/out_complex_multi_module_sm/click.kamil.domain.StateMachineConfig/click.kamil.domain.StateMachineConfig.html @@ -0,0 +1,1012 @@ + + + + + + State Machine Explorer - click.kamil.domain.StateMachineConfig + + + + + + + + + + + + + + + + +
+ +
+ OrderState.NEWOrderState.PROCESSINGOrderState.COMPLETEDOrderState.CANCELLEDOrderEvent.PROCESSOrderEvent.COMPLETEOrderEvent.CANCELOrderEvent.CANCEL +
+
+ + + + + + diff --git a/out_complex_multi_module_sm/click.kamil.domain.StateMachineConfig/click.kamil.domain.StateMachineConfig.json b/out_complex_multi_module_sm/click.kamil.domain.StateMachineConfig/click.kamil.domain.StateMachineConfig.json new file mode 100644 index 0000000..b7d9930 --- /dev/null +++ b/out_complex_multi_module_sm/click.kamil.domain.StateMachineConfig/click.kamil.domain.StateMachineConfig.json @@ -0,0 +1,330 @@ +{ + "metadata" : { + "triggers" : [ { + "event" : "event", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 25 + }, { + "event" : "eventProvider", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendMessageWithProvider", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 50 + } ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/orders/process", + "className" : "click.kamil.web.OrderController", + "methodName" : "processOrder", + "sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java", + "metadata" : { + "path" : "/api/orders/process", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /api/orders/complete", + "className" : "click.kamil.web.OrderController", + "methodName" : "completeOrder", + "sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java", + "metadata" : { + "path" : "/api/orders/complete", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "REST", + "name" : "POST /api/orders/cancel", + "className" : "click.kamil.web.OrderController", + "methodName" : "cancelOrder", + "sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java", + "metadata" : { + "path" : "/api/orders/cancel", + "verb" : "POST" + }, + "parameters" : [ ] + }, { + "type" : "JMS", + "name" : "JMS: order.process.queue", + "className" : "click.kamil.web.JmsOrderListener", + "methodName" : "receiveProcessCommand", + "sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java", + "metadata" : { + "protocol" : "JMS", + "destination" : "order.process.queue" + }, + "parameters" : [ { + "name" : "message", + "type" : "String", + "annotations" : [ ] + } ] + }, { + "type" : "JMS", + "name" : "JMS: order.cancel.queue", + "className" : "click.kamil.web.JmsOrderListener", + "methodName" : "receiveCancelCommand", + "sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java", + "metadata" : { + "protocol" : "JMS", + "destination" : "order.cancel.queue" + }, + "parameters" : [ { + "name" : "message", + "type" : "String", + "annotations" : [ ] + } ] + } ], + "callChains" : [ { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/process", + "className" : "click.kamil.web.OrderController", + "methodName" : "processOrder", + "sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java", + "metadata" : { + "path" : "/api/orders/process", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.web.OrderController.processOrder", "click.kamil.service.StateMachineServiceImpl.sendMessageWithOutbox", "click.kamil.service.StateMachineServiceImpl.sendMessage" ], + "triggerPoint" : { + "event" : "OrderEvent.PROCESS", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 25 + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderState.NEW", + "targetState" : "OrderState.PROCESSING", + "event" : "OrderEvent.PROCESS" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/complete", + "className" : "click.kamil.web.OrderController", + "methodName" : "completeOrder", + "sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java", + "metadata" : { + "path" : "/api/orders/complete", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.web.OrderController.completeOrder", "click.kamil.service.StateMachineServiceImpl.sendMessage" ], + "triggerPoint" : { + "event" : "OrderEvent.COMPLETE", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 25 + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderState.PROCESSING", + "targetState" : "OrderState.COMPLETED", + "event" : "OrderEvent.COMPLETE" + } ] + }, { + "entryPoint" : { + "type" : "REST", + "name" : "POST /api/orders/cancel", + "className" : "click.kamil.web.OrderController", + "methodName" : "cancelOrder", + "sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java", + "metadata" : { + "path" : "/api/orders/cancel", + "verb" : "POST" + }, + "parameters" : [ ] + }, + "methodChain" : [ "click.kamil.web.OrderController.cancelOrder", "click.kamil.service.StateMachineServiceImpl.sendMessage" ], + "triggerPoint" : { + "event" : "OrderEvent.CANCEL", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 25 + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderState.NEW", + "targetState" : "OrderState.CANCELLED", + "event" : "OrderEvent.CANCEL" + }, { + "sourceState" : "OrderState.PROCESSING", + "targetState" : "OrderState.CANCELLED", + "event" : "OrderEvent.CANCEL" + } ] + }, { + "entryPoint" : { + "type" : "JMS", + "name" : "JMS: order.process.queue", + "className" : "click.kamil.web.JmsOrderListener", + "methodName" : "receiveProcessCommand", + "sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java", + "metadata" : { + "protocol" : "JMS", + "destination" : "order.process.queue" + }, + "parameters" : [ { + "name" : "message", + "type" : "String", + "annotations" : [ ] + } ] + }, + "methodChain" : [ "click.kamil.web.JmsOrderListener.receiveProcessCommand", "click.kamil.service.StateMachineServiceImpl.sendMessageWithOutbox", "click.kamil.service.StateMachineServiceImpl.sendMessage" ], + "triggerPoint" : { + "event" : "OrderEvent.PROCESS", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 25 + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderState.NEW", + "targetState" : "OrderState.PROCESSING", + "event" : "OrderEvent.PROCESS" + } ] + }, { + "entryPoint" : { + "type" : "JMS", + "name" : "JMS: order.cancel.queue", + "className" : "click.kamil.web.JmsOrderListener", + "methodName" : "receiveCancelCommand", + "sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java", + "metadata" : { + "protocol" : "JMS", + "destination" : "order.cancel.queue" + }, + "parameters" : [ { + "name" : "message", + "type" : "String", + "annotations" : [ ] + } ] + }, + "methodChain" : [ "click.kamil.web.JmsOrderListener.receiveCancelCommand", "click.kamil.service.StateMachineServiceImpl.sendMessageWithProvider" ], + "triggerPoint" : { + "event" : "OrderEvent.CANCEL", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendMessageWithProvider", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 50 + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderState.NEW", + "targetState" : "OrderState.CANCELLED", + "event" : "OrderEvent.CANCEL" + }, { + "sourceState" : "OrderState.PROCESSING", + "targetState" : "OrderState.CANCELLED", + "event" : "OrderEvent.CANCEL" + } ] + } ], + "properties" : { + "default" : { } + } + }, + "name" : "click.kamil.domain.StateMachineConfig", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "OrderState.NEW" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderState.NEW", + "fullIdentifier" : "OrderState.NEW" + } ], + "targetStates" : [ { + "rawName" : "OrderState.PROCESSING", + "fullIdentifier" : "OrderState.PROCESSING" + } ], + "event" : { + "rawName" : "OrderEvent.PROCESS", + "fullIdentifier" : "OrderEvent.PROCESS" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderState.PROCESSING", + "fullIdentifier" : "OrderState.PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "OrderState.COMPLETED", + "fullIdentifier" : "OrderState.COMPLETED" + } ], + "event" : { + "rawName" : "OrderEvent.COMPLETE", + "fullIdentifier" : "OrderEvent.COMPLETE" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderState.NEW", + "fullIdentifier" : "OrderState.NEW" + } ], + "targetStates" : [ { + "rawName" : "OrderState.CANCELLED", + "fullIdentifier" : "OrderState.CANCELLED" + } ], + "event" : { + "rawName" : "OrderEvent.CANCEL", + "fullIdentifier" : "OrderEvent.CANCEL" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderState.PROCESSING", + "fullIdentifier" : "OrderState.PROCESSING" + } ], + "targetStates" : [ { + "rawName" : "OrderState.CANCELLED", + "fullIdentifier" : "OrderState.CANCELLED" + } ], + "event" : { + "rawName" : "OrderEvent.CANCEL", + "fullIdentifier" : "OrderEvent.CANCEL" + }, + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "OrderState.COMPLETED", "OrderState.CANCELLED" ] +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java index d99620d..5f6d307 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java @@ -58,47 +58,73 @@ public class CallGraphBuilder { String event = tp.getEvent(); if (event == null || event.isEmpty() || event.contains("\"")) return tp; - TypeDeclaration td = context.getTypeDeclaration(tp.getClassName()); - if (td != null) { - MethodDeclaration md = context.findMethodDeclaration(td, tp.getMethodName(), true); - if (md != null) { - int paramIndex = -1; - for (int i = 0; i < md.parameters().size(); i++) { - SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i); - if (svd.getName().getIdentifier().equals(event)) { - paramIndex = i; - break; - } - } - - if (paramIndex >= 0) { - String caller = path.get(path.size() - 2); - String target = path.get(path.size() - 1); - List edges = callGraph.get(caller); - if (edges != null) { - for (CallEdge edge : edges) { - if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { - if (paramIndex < edge.getArguments().size()) { - String resolvedValue = edge.getArguments().get(paramIndex); - if (resolvedValue != null) { - return TriggerPoint.builder() - .event(resolvedValue) - .className(tp.getClassName()) - .methodName(tp.getMethodName()) - .sourceFile(tp.getSourceFile()) - .lineNumber(tp.getLineNumber()) - .build(); - } - } + String currentParamName = event; + String resolvedValue = event; + + // Walk backwards up the call chain + for (int i = path.size() - 1; i > 0; i--) { + String target = path.get(i); + String caller = path.get(i - 1); + + // Find parameter index in target method + int paramIndex = getParameterIndex(target, currentParamName); + if (paramIndex < 0) { + break; // Parameter name changed or not found, stop tracing + } + + // Find the edge from caller to target to get the argument passed + List edges = callGraph.get(caller); + boolean found = false; + if (edges != null) { + for (CallEdge edge : edges) { + if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { + if (paramIndex < edge.getArguments().size()) { + String arg = edge.getArguments().get(paramIndex); + if (arg != null) { + currentParamName = arg; + resolvedValue = arg; + found = true; + break; } } } } } + if (!found) break; // Could not map argument } + + if (!resolvedValue.equals(event)) { + return TriggerPoint.builder() + .event(resolvedValue) + .className(tp.getClassName()) + .methodName(tp.getMethodName()) + .sourceFile(tp.getSourceFile()) + .lineNumber(tp.getLineNumber()) + .build(); + } + return tp; } + private int getParameterIndex(String methodFqn, String paramName) { + if (methodFqn == null || !methodFqn.contains(".")) return -1; + String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); + TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null) { + for (int i = 0; i < md.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i); + if (svd.getName().getIdentifier().equals(paramName)) { + return i; + } + } + } + } + return -1; + } + private String extractContextMachineId(List path, Map> callGraph) { for (String node : path) { List edges = callGraph.get(node); @@ -174,6 +200,22 @@ public class CallGraphBuilder { List args = new ArrayList<>(); for (Object argObj : astArguments) { Expression expr = (Expression) argObj; + + // Extract from lambda + if (expr instanceof LambdaExpression le) { + ASTNode body = le.getBody(); + if (body instanceof Expression bodyExpr) { + expr = bodyExpr; + } else if (body instanceof Block block) { + for (Object stmtObj : block.statements()) { + if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) { + expr = rs.getExpression(); + break; + } + } + } + } + String val = constantResolver.resolve(expr, context); if (val == null) { val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc) 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 5bfd7c8..5e1acd7 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 @@ -240,10 +240,22 @@ public class GenericEventDetector { if (resolved != null) return resolved; // If not a constant, it might be a parameter like 'event' + if (payloadExpr instanceof CastExpression ce) { + payloadExpr = ce.getExpression(); + } + if (payloadExpr instanceof SimpleName sn) { - return sn.getIdentifier(); // Return the parameter name so the call graph can fill it + String extracted = extractEventFromMessageBuilder(payloadExpr); + if (extracted != null) { + return extracted; + } + return sn.getIdentifier(); // Fall back to returning the parameter name } return payloadExpr.toString(); + } else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) { + // If the event is obtained by calling a method on a provider/supplier parameter, + // return the provider's name so CallGraphBuilder can trace the lambda argument + return sn.getIdentifier(); } Expression receiver = current.getExpression(); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/MessagingDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/MessagingDetector.java index beffa1b..efd1964 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/MessagingDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/MessagingDetector.java @@ -71,6 +71,7 @@ public class MessagingDetector { .name(protocolName + ": " + destination) .className(context.getFqn((TypeDeclaration) method.getParent())) .methodName(method.getName().getIdentifier()) + .sourceFile(context.getRelativePath(context.getFqn((TypeDeclaration) method.getParent()))) .metadata(metadata) .parameters(extractParameters(method)) .build()); 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 4f4cc7d..e9e53f6 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 @@ -230,7 +230,20 @@ public class CodebaseContext { // Try FQN match if input was simple name String fqn = simpleNameToFqn.get(interfaceName); - if (fqn != null) return interfaceToImpls.getOrDefault(fqn, Collections.emptyList()); + if (fqn != null && interfaceToImpls.containsKey(fqn)) { + return interfaceToImpls.get(fqn); + } + + // Try simple name match if input was FQN + if (interfaceName.contains(".")) { + String simpleName = interfaceName.substring(interfaceName.lastIndexOf('.') + 1); + List simpleImpls = interfaceToImpls.get(simpleName); + if (simpleImpls != null) { + // To be perfectly safe, we could check if simpleNameToFqn matches, + // but for call graphs, returning potential implementations is fine. + return simpleImpls; + } + } return Collections.emptyList(); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java index 7a300bc..22f8c4e 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java @@ -32,6 +32,9 @@ public class FileUtils { return paths .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension)) .filter(p -> matchers.stream().noneMatch(m -> m.matches(p))) + .map(Path::toAbsolutePath) + .map(Path::normalize) + .distinct() .collect(Collectors.toList()); } } diff --git a/state_machine_exporter_html/src/main/resources/html/template.html b/state_machine_exporter_html/src/main/resources/html/template.html index f8b42b0..7b44a4e 100644 --- a/state_machine_exporter_html/src/main/resources/html/template.html +++ b/state_machine_exporter_html/src/main/resources/html/template.html @@ -576,7 +576,7 @@ if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false; return c.matchedTransitions.some(t => { const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event; - return normalize(cEventStr) === name; + return normalize(cEventStr) === normalize(name); }); }); @@ -631,7 +631,7 @@ let html = `
Input Data
`; html += `
`; params.forEach(p => { - const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam")); + const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam") || a.endsWith("Payload") || a.endsWith("Header") || a.endsWith("Headers")); const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : ""; html += `
${cleanAnn} diff --git a/state_machines/complex_multi_module_sm/.gitignore b/state_machines/complex_multi_module_sm/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/state_machines/complex_multi_module_sm/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/state_machines/complex_multi_module_sm/domain/.gitignore b/state_machines/complex_multi_module_sm/domain/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/state_machines/complex_multi_module_sm/domain/pom.xml b/state_machines/complex_multi_module_sm/domain/pom.xml new file mode 100644 index 0000000..f052b12 --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + click.kamil.complex + complex-multi-module-sm + 1.0.0 + + + domain + + + + org.springframework.statemachine + spring-statemachine-core + + + org.springframework.boot + spring-boot-starter + + + diff --git a/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/IEvent.java b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/IEvent.java new file mode 100644 index 0000000..0ec9c09 --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/IEvent.java @@ -0,0 +1,4 @@ +package click.kamil.domain; + +public interface IEvent { +} diff --git a/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/IState.java b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/IState.java new file mode 100644 index 0000000..18bbe91 --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/IState.java @@ -0,0 +1,4 @@ +package click.kamil.domain; + +public interface IState { +} diff --git a/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/OrderEvent.java b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/OrderEvent.java new file mode 100644 index 0000000..bc4c890 --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/OrderEvent.java @@ -0,0 +1,5 @@ +package click.kamil.domain; + +public enum OrderEvent implements IEvent { + PROCESS, COMPLETE, CANCEL +} diff --git a/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/OrderState.java b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/OrderState.java new file mode 100644 index 0000000..ceec376 --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/OrderState.java @@ -0,0 +1,5 @@ +package click.kamil.domain; + +public enum OrderState implements IState { + NEW, PROCESSING, COMPLETED, CANCELLED +} diff --git a/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/StateMachineConfig.java b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/StateMachineConfig.java new file mode 100644 index 0000000..5b79aef --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/StateMachineConfig.java @@ -0,0 +1,38 @@ +package click.kamil.domain; + +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; + +import java.util.EnumSet; + +@Configuration +@EnableStateMachine +public class StateMachineConfig extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(OrderState.NEW) + .states(EnumSet.allOf(OrderState.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(OrderState.NEW).target(OrderState.PROCESSING).event(OrderEvent.PROCESS) + .and() + .withExternal() + .source(OrderState.PROCESSING).target(OrderState.COMPLETED).event(OrderEvent.COMPLETE) + .and() + .withExternal() + .source(OrderState.NEW).target(OrderState.CANCELLED).event(OrderEvent.CANCEL) + .and() + .withExternal() + .source(OrderState.PROCESSING).target(OrderState.CANCELLED).event(OrderEvent.CANCEL); + } +} diff --git a/state_machines/complex_multi_module_sm/pom.xml b/state_machines/complex_multi_module_sm/pom.xml new file mode 100644 index 0000000..3214e2d --- /dev/null +++ b/state_machines/complex_multi_module_sm/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + click.kamil.complex + complex-multi-module-sm + 1.0.0 + pom + + + domain + service + web + + + + 21 + 21 + 3.2.0 + 3.2.0 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + org.springframework.statemachine + spring-statemachine-core + ${spring-statemachine.version} + + + click.kamil.complex + domain + ${project.version} + + + click.kamil.complex + service + ${project.version} + + + + diff --git a/state_machines/complex_multi_module_sm/service/.gitignore b/state_machines/complex_multi_module_sm/service/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/state_machines/complex_multi_module_sm/service/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/state_machines/complex_multi_module_sm/service/pom.xml b/state_machines/complex_multi_module_sm/service/pom.xml new file mode 100644 index 0000000..e6bfa3b --- /dev/null +++ b/state_machines/complex_multi_module_sm/service/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + click.kamil.complex + complex-multi-module-sm + 1.0.0 + + + service + + + + click.kamil.complex + domain + + + org.springframework.boot + spring-boot-starter + + + org.springframework.statemachine + spring-statemachine-core + + + org.springframework + spring-tx + + + diff --git a/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/OutboxAction.java b/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/OutboxAction.java new file mode 100644 index 0000000..a82d94d --- /dev/null +++ b/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/OutboxAction.java @@ -0,0 +1,7 @@ +package click.kamil.service; + +import click.kamil.domain.IEvent; + +public interface OutboxAction { + void onCommit(T event); +} diff --git a/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/StateMachineService.java b/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/StateMachineService.java new file mode 100644 index 0000000..e49aa2e --- /dev/null +++ b/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/StateMachineService.java @@ -0,0 +1,19 @@ +package click.kamil.service; + +import click.kamil.domain.IEvent; +import click.kamil.domain.IState; + +public interface StateMachineService { + void sendMessage(T event); + + > void sendMessageWithOutbox(T event, A action); + + // Quirky pattern: Bounded generic supplier instead of reflection + void sendMessageWithProvider(java.util.function.Supplier eventProvider); + + // Quirky pattern 2: Generic Varargs + void sendEventsVarargs(T... events); + + // Quirky pattern 3: Multiple generic bounds (Intersection Types) + T processQuirkyEvent(T event); +} diff --git a/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/StateMachineServiceImpl.java b/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/StateMachineServiceImpl.java new file mode 100644 index 0000000..a1470e9 --- /dev/null +++ b/state_machines/complex_multi_module_sm/service/src/main/java/click/kamil/service/StateMachineServiceImpl.java @@ -0,0 +1,69 @@ +package click.kamil.service; + +import click.kamil.domain.IEvent; +import click.kamil.domain.IState; +import click.kamil.domain.OrderEvent; +import click.kamil.domain.OrderState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.StateMachine; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import reactor.core.publisher.Mono; + +@Service +public class StateMachineServiceImpl implements StateMachineService { + + @Autowired + private StateMachine stateMachine; + + @Override + public void sendMessage(T event) { + if (event instanceof OrderEvent) { + stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload((OrderEvent) event).build())).subscribe(); + } + } + + @Override + @Transactional + public > void sendMessageWithOutbox(T event, A action) { + sendMessage(event); + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + action.onCommit(event); + } + }); + } else { + action.onCommit(event); + } + } + + @Override + public void sendMessageWithProvider(java.util.function.Supplier eventProvider) { + T event = eventProvider.get(); + if (event != null) { + stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload((OrderEvent) event).build())).subscribe(); + } + } + + @Override + @SafeVarargs + public final void sendEventsVarargs(T... events) { + // Quirky pattern 2: Iterating over generic varargs + for (T event : events) { + sendMessage(event); + } + } + + @Override + public T processQuirkyEvent(T event) { + // Quirky pattern 3: Multiple bounds, returning the same generic type + sendMessage(event); + return event; // returns something that is guaranteed to be IEvent, Serializable, AND Cloneable + } +} diff --git a/state_machines/complex_multi_module_sm/web/.gitignore b/state_machines/complex_multi_module_sm/web/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/state_machines/complex_multi_module_sm/web/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/state_machines/complex_multi_module_sm/web/pom.xml b/state_machines/complex_multi_module_sm/web/pom.xml new file mode 100644 index 0000000..aba55cb --- /dev/null +++ b/state_machines/complex_multi_module_sm/web/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + click.kamil.complex + complex-multi-module-sm + 1.0.0 + + + web + + + + click.kamil.complex + service + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-activemq + + + diff --git a/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/Application.java b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/Application.java new file mode 100644 index 0000000..5de38dc --- /dev/null +++ b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/Application.java @@ -0,0 +1,14 @@ +package click.kamil.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jms.annotation.EnableJms; + +@SpringBootApplication(scanBasePackages = "click.kamil") +@EnableJms +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/JmsOrderListener.java b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/JmsOrderListener.java new file mode 100644 index 0000000..1977dfd --- /dev/null +++ b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/JmsOrderListener.java @@ -0,0 +1,31 @@ +package click.kamil.web; + +import click.kamil.domain.OrderEvent; +import click.kamil.service.StateMachineService; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class JmsOrderListener { + + private final StateMachineService stateMachineService; + + // Constructor injection + public JmsOrderListener(StateMachineService stateMachineService) { + this.stateMachineService = stateMachineService; + } + + @JmsListener(destination = "order.process.queue") + public void receiveProcessCommand(String message) { + System.out.println("Received JMS message: " + message); + stateMachineService.sendMessageWithOutbox(OrderEvent.PROCESS, event -> { + System.out.println("JMS Triggered Faulty Outbox Action executed post-commit for event: " + event); + }); + } + + @JmsListener(destination = "order.cancel.queue") + public void receiveCancelCommand(String message) { + System.out.println("Received JMS message: " + message); + stateMachineService.sendMessageWithProvider(() -> OrderEvent.CANCEL); + } +} diff --git a/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/OrderController.java b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/OrderController.java new file mode 100644 index 0000000..4d0ba24 --- /dev/null +++ b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/OrderController.java @@ -0,0 +1,37 @@ +package click.kamil.web; + +import click.kamil.domain.OrderEvent; +import click.kamil.service.StateMachineService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/orders") +public class OrderController { + + @Autowired + private StateMachineService stateMachineService; + + @PostMapping("/process") + public String processOrder() { + stateMachineService.sendMessageWithOutbox(OrderEvent.PROCESS, event -> { + System.out.println("Faulty Outbox Action executed post-commit for event: " + event); + }); + return "Order process event sent within transaction"; + } + + @PostMapping("/complete") + public String completeOrder() { + stateMachineService.sendMessage(OrderEvent.COMPLETE); + return "Order complete event sent"; + } + + @PostMapping("/cancel") + public String cancelOrder() { + stateMachineService.sendMessage(OrderEvent.CANCEL); + return "Order cancel event sent"; + } +}