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 index f6408af..0dda9bd 100644 --- 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 @@ -400,6 +400,15 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 50 + }, { + "event" : "customMessage", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendCustomMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 69 } ], "entryPoints" : [ { "type" : "REST", @@ -434,6 +443,21 @@ "verb" : "POST" }, "parameters" : [ ] + }, { + "type" : "RABBIT", + "name" : "RABBIT: order.custom.transition.queue", + "className" : "click.kamil.web.RabbitOrderListener", + "methodName" : "handleCustomTransition", + "sourceFile" : "web/src/main/java/click/kamil/web/RabbitOrderListener.java", + "metadata" : { + "protocol" : "RABBIT", + "destination" : "order.custom.transition.queue" + }, + "parameters" : [ { + "name" : "payload", + "type" : "String", + "annotations" : [ ] + } ] }, { "type" : "JMS", "name" : "JMS: order.process.queue", @@ -559,6 +583,40 @@ "targetState" : "OrderState.CANCELLED", "event" : "OrderEvent.CANCEL" } ] + }, { + "entryPoint" : { + "type" : "RABBIT", + "name" : "RABBIT: order.custom.transition.queue", + "className" : "click.kamil.web.RabbitOrderListener", + "methodName" : "handleCustomTransition", + "sourceFile" : "web/src/main/java/click/kamil/web/RabbitOrderListener.java", + "metadata" : { + "protocol" : "RABBIT", + "destination" : "order.custom.transition.queue" + }, + "parameters" : [ { + "name" : "payload", + "type" : "String", + "annotations" : [ ] + } ] + }, + "methodChain" : [ "click.kamil.web.RabbitOrderListener.handleCustomTransition", "click.kamil.service.StateMachineServiceImpl.sendCustomMessage" ], + "triggerPoint" : { + "event" : "OrderEvent.PROCESS", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendCustomMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 69 + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderState.NEW", + "targetState" : "OrderState.PROCESSING", + "event" : "OrderEvent.PROCESS" + } ] }, { "entryPoint" : { "type" : "JMS", 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 index b7d9930..0ab17ae 100644 --- 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 @@ -18,6 +18,15 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 50 + }, { + "event" : "customMessage", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendCustomMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 69 } ], "entryPoints" : [ { "type" : "REST", @@ -52,6 +61,21 @@ "verb" : "POST" }, "parameters" : [ ] + }, { + "type" : "RABBIT", + "name" : "RABBIT: order.custom.transition.queue", + "className" : "click.kamil.web.RabbitOrderListener", + "methodName" : "handleCustomTransition", + "sourceFile" : "web/src/main/java/click/kamil/web/RabbitOrderListener.java", + "metadata" : { + "protocol" : "RABBIT", + "destination" : "order.custom.transition.queue" + }, + "parameters" : [ { + "name" : "payload", + "type" : "String", + "annotations" : [ ] + } ] }, { "type" : "JMS", "name" : "JMS: order.process.queue", @@ -177,6 +201,40 @@ "targetState" : "OrderState.CANCELLED", "event" : "OrderEvent.CANCEL" } ] + }, { + "entryPoint" : { + "type" : "RABBIT", + "name" : "RABBIT: order.custom.transition.queue", + "className" : "click.kamil.web.RabbitOrderListener", + "methodName" : "handleCustomTransition", + "sourceFile" : "web/src/main/java/click/kamil/web/RabbitOrderListener.java", + "metadata" : { + "protocol" : "RABBIT", + "destination" : "order.custom.transition.queue" + }, + "parameters" : [ { + "name" : "payload", + "type" : "String", + "annotations" : [ ] + } ] + }, + "methodChain" : [ "click.kamil.web.RabbitOrderListener.handleCustomTransition", "click.kamil.service.StateMachineServiceImpl.sendCustomMessage" ], + "triggerPoint" : { + "event" : "OrderEvent.PROCESS", + "className" : "click.kamil.service.StateMachineServiceImpl", + "methodName" : "sendCustomMessage", + "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", + "sourceModule" : null, + "stateMachineId" : null, + "sourceState" : null, + "lineNumber" : 69 + }, + "contextMachineId" : null, + "matchedTransitions" : [ { + "sourceState" : "OrderState.NEW", + "targetState" : "OrderState.PROCESSING", + "event" : "OrderEvent.PROCESS" + } ] }, { "entryPoint" : { "type" : "JMS", 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 5f6d307..75d3b11 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 @@ -216,6 +216,14 @@ public class CallGraphBuilder { } } + // Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...)) + expr = traceVariable(expr); + if (expr instanceof ClassInstanceCreation cic) { + if (!cic.arguments().isEmpty()) { + expr = (Expression) cic.arguments().get(0); + } + } + String val = constantResolver.resolve(expr, context); if (val == null) { val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc) @@ -317,4 +325,41 @@ public class CallGraphBuilder { } return (TypeDeclaration) parent; } + private Expression traceVariable(Expression expr) { + if (expr instanceof SimpleName sn) { + String varName = sn.getIdentifier(); + MethodDeclaration enclosingMethod = findEnclosingMethod(expr); + if (enclosingMethod != null) { + final Expression[] initializer = new Expression[1]; + enclosingMethod.accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationFragment node) { + if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { + initializer[0] = node.getInitializer(); + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { + initializer[0] = node.getRightHandSide(); + } + return super.visit(node); + } + }); + if (initializer[0] != null) { + return traceVariable(initializer[0]); + } + } + } + return expr; + } + + private MethodDeclaration findEnclosingMethod(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof MethodDeclaration)) { + parent = parent.getParent(); + } + return (MethodDeclaration) parent; + } } 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 5e1acd7..ba33d05 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 @@ -193,6 +193,10 @@ public class GenericEventDetector { } private String extractEventFromMessageBuilder(Expression expr) { + if (expr instanceof CastExpression ce) { + return extractEventFromMessageBuilder(ce.getExpression()); + } + if (expr instanceof SimpleName sn) { String varName = sn.getIdentifier(); MethodDeclaration enclosingMethod = findEnclosingMethod(expr); @@ -218,6 +222,15 @@ public class GenericEventDetector { if (initializer[0] != null) { return extractEventFromMessageBuilder(initializer[0]); // recursive } + + // If it has NO initializer, it might be a method parameter! + for (Object paramObj : enclosingMethod.parameters()) { + if (paramObj instanceof SingleVariableDeclaration svd) { + if (svd.getName().getIdentifier().equals(varName)) { + return varName; + } + } + } } } diff --git a/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/CustomStateMachineMessage.java b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/CustomStateMachineMessage.java new file mode 100644 index 0000000..ca2ae4d --- /dev/null +++ b/state_machines/complex_multi_module_sm/domain/src/main/java/click/kamil/domain/CustomStateMachineMessage.java @@ -0,0 +1,24 @@ +package click.kamil.domain; + +import org.springframework.messaging.support.GenericMessage; + +import java.util.Map; + +public class CustomStateMachineMessage extends GenericMessage { + + private final P customPayload; + + public CustomStateMachineMessage(T payload, P customPayload) { + super(payload); + this.customPayload = customPayload; + } + + public CustomStateMachineMessage(T payload, Map headers, P customPayload) { + super(payload, headers); + this.customPayload = customPayload; + } + + public P getCustomPayload() { + return customPayload; + } +} 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 index e49aa2e..23f4f92 100644 --- 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 @@ -14,6 +14,9 @@ public interface StateMachineService { // Quirky pattern 2: Generic Varargs void sendEventsVarargs(T... events); + // Child class of Spring's GenericMessage with a generic payload + void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage customMessage); + // 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 index a1470e9..f7dd0a5 100644 --- 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 @@ -54,12 +54,21 @@ public class StateMachineServiceImpl implements StateMachineService { @Override @SafeVarargs public final void sendEventsVarargs(T... events) { - // Quirky pattern 2: Iterating over generic varargs for (T event : events) { sendMessage(event); } } + @Override + public void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage customMessage) { + // Because CustomStateMachineMessage extends GenericMessage and implements Message, + // we can pass it directly to the state machine as long as T resolves to OrderEvent! + + // We cast it to Message to satisfy Mono's strict type requirements + org.springframework.messaging.Message msg = (org.springframework.messaging.Message) customMessage; + stateMachine.sendEvent(Mono.just(msg)).subscribe(); + } + @Override public T processQuirkyEvent(T event) { // Quirky pattern 3: Multiple bounds, returning the same generic type diff --git a/state_machines/complex_multi_module_sm/web/pom.xml b/state_machines/complex_multi_module_sm/web/pom.xml index aba55cb..4488baf 100644 --- a/state_machines/complex_multi_module_sm/web/pom.xml +++ b/state_machines/complex_multi_module_sm/web/pom.xml @@ -24,5 +24,9 @@ org.springframework.boot spring-boot-starter-activemq + + org.springframework.boot + spring-boot-starter-amqp + diff --git a/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/RabbitOrderListener.java b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/RabbitOrderListener.java new file mode 100644 index 0000000..cb83ebf --- /dev/null +++ b/state_machines/complex_multi_module_sm/web/src/main/java/click/kamil/web/RabbitOrderListener.java @@ -0,0 +1,32 @@ +package click.kamil.web; + +import click.kamil.domain.CustomStateMachineMessage; +import click.kamil.domain.OrderEvent; +import click.kamil.service.StateMachineService; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class RabbitOrderListener { + + private final StateMachineService stateMachineService; + + // Constructor Injection + public RabbitOrderListener(StateMachineService stateMachineService) { + this.stateMachineService = stateMachineService; + } + + // Listens to RabbitMQ, constructs the custom class, triggers a state transition + @RabbitListener(queues = "order.custom.transition.queue") + public void handleCustomTransition(String payload) { + System.out.println("Received RabbitMQ message: " + payload); + + // Construct our custom event containing the generic payload and the enum state + CustomStateMachineMessage customMessage = + new CustomStateMachineMessage<>(OrderEvent.PROCESS, "My Rabbit Custom Payload: " + payload); + + // Push the custom event into the state machine to perform the transition + stateMachineService.sendCustomMessage(customMessage); + System.out.println("Triggered PROCESS transition via custom message over RabbitMQ"); + } +}