2 Commits

Author SHA1 Message Date
b480dc83ef transition enricher rabbit 2026-06-19 09:22:27 +02:00
0a23daae05 transition enricher 2026-06-19 09:06:37 +02:00
29 changed files with 2195 additions and 36 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,388 @@
{
"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
}, {
"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",
"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" : "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",
"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" : "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",
"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" ]
}

View File

@@ -58,29 +58,42 @@ public class CallGraphBuilder {
String event = tp.getEvent(); String event = tp.getEvent();
if (event == null || event.isEmpty() || event.contains("\"")) return tp; if (event == null || event.isEmpty() || event.contains("\"")) return tp;
TypeDeclaration td = context.getTypeDeclaration(tp.getClassName()); String currentParamName = event;
if (td != null) { String resolvedValue = event;
MethodDeclaration md = context.findMethodDeclaration(td, tp.getMethodName(), true);
if (md != null) { // Walk backwards up the call chain
int paramIndex = -1; for (int i = path.size() - 1; i > 0; i--) {
for (int i = 0; i < md.parameters().size(); i++) { String target = path.get(i);
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i); String caller = path.get(i - 1);
if (svd.getName().getIdentifier().equals(event)) {
paramIndex = i; // Find parameter index in target method
break; int paramIndex = getParameterIndex(target, currentParamName);
} if (paramIndex < 0) {
break; // Parameter name changed or not found, stop tracing
} }
if (paramIndex >= 0) { // Find the edge from caller to target to get the argument passed
String caller = path.get(path.size() - 2);
String target = path.get(path.size() - 1);
List<CallEdge> edges = callGraph.get(caller); List<CallEdge> edges = callGraph.get(caller);
boolean found = false;
if (edges != null) { if (edges != null) {
for (CallEdge edge : edges) { for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
if (paramIndex < edge.getArguments().size()) { if (paramIndex < edge.getArguments().size()) {
String resolvedValue = edge.getArguments().get(paramIndex); String arg = edge.getArguments().get(paramIndex);
if (resolvedValue != null) { if (arg != null) {
currentParamName = arg;
resolvedValue = arg;
found = true;
break;
}
}
}
}
}
if (!found) break; // Could not map argument
}
if (!resolvedValue.equals(event)) {
return TriggerPoint.builder() return TriggerPoint.builder()
.event(resolvedValue) .event(resolvedValue)
.className(tp.getClassName()) .className(tp.getClassName())
@@ -89,16 +102,29 @@ public class CallGraphBuilder {
.lineNumber(tp.getLineNumber()) .lineNumber(tp.getLineNumber())
.build(); .build();
} }
}
}
}
}
}
}
}
return tp; 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<String> path, Map<String, List<CallEdge>> callGraph) { private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
for (String node : path) { for (String node : path) {
List<CallEdge> edges = callGraph.get(node); List<CallEdge> edges = callGraph.get(node);
@@ -174,6 +200,30 @@ public class CallGraphBuilder {
List<String> args = new ArrayList<>(); List<String> args = new ArrayList<>();
for (Object argObj : astArguments) { for (Object argObj : astArguments) {
Expression expr = (Expression) argObj; 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;
}
}
}
}
// 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); String val = constantResolver.resolve(expr, context);
if (val == null) { if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc) val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
@@ -275,4 +325,41 @@ public class CallGraphBuilder {
} }
return (TypeDeclaration) parent; 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;
}
} }

View File

@@ -193,6 +193,10 @@ public class GenericEventDetector {
} }
private String extractEventFromMessageBuilder(Expression expr) { private String extractEventFromMessageBuilder(Expression expr) {
if (expr instanceof CastExpression ce) {
return extractEventFromMessageBuilder(ce.getExpression());
}
if (expr instanceof SimpleName sn) { if (expr instanceof SimpleName sn) {
String varName = sn.getIdentifier(); String varName = sn.getIdentifier();
MethodDeclaration enclosingMethod = findEnclosingMethod(expr); MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
@@ -218,6 +222,15 @@ public class GenericEventDetector {
if (initializer[0] != null) { if (initializer[0] != null) {
return extractEventFromMessageBuilder(initializer[0]); // recursive 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;
}
}
}
} }
} }
@@ -240,10 +253,22 @@ public class GenericEventDetector {
if (resolved != null) return resolved; if (resolved != null) return resolved;
// If not a constant, it might be a parameter like 'event' // If not a constant, it might be a parameter like 'event'
if (payloadExpr instanceof CastExpression ce) {
payloadExpr = ce.getExpression();
}
if (payloadExpr instanceof SimpleName sn) { 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(); 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(); Expression receiver = current.getExpression();

View File

@@ -71,6 +71,7 @@ public class MessagingDetector {
.name(protocolName + ": " + destination) .name(protocolName + ": " + destination)
.className(context.getFqn((TypeDeclaration) method.getParent())) .className(context.getFqn((TypeDeclaration) method.getParent()))
.methodName(method.getName().getIdentifier()) .methodName(method.getName().getIdentifier())
.sourceFile(context.getRelativePath(context.getFqn((TypeDeclaration) method.getParent())))
.metadata(metadata) .metadata(metadata)
.parameters(extractParameters(method)) .parameters(extractParameters(method))
.build()); .build());

View File

@@ -230,7 +230,20 @@ public class CodebaseContext {
// Try FQN match if input was simple name // Try FQN match if input was simple name
String fqn = simpleNameToFqn.get(interfaceName); 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<String> 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(); return Collections.emptyList();
} }

View File

@@ -32,6 +32,9 @@ public class FileUtils {
return paths return paths
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension)) .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
.filter(p -> matchers.stream().noneMatch(m -> m.matches(p))) .filter(p -> matchers.stream().noneMatch(m -> m.matches(p)))
.map(Path::toAbsolutePath)
.map(Path::normalize)
.distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }

View File

@@ -576,7 +576,7 @@
if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false; if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false;
return c.matchedTransitions.some(t => { return c.matchedTransitions.some(t => {
const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event; 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 = `<div style="font-size:0.65rem; font-weight:700; color:#94a3b8; margin-top:8px; margin-bottom:4px; text-transform:uppercase;">Input Data</div>`; let html = `<div style="font-size:0.65rem; font-weight:700; color:#94a3b8; margin-top:8px; margin-bottom:4px; text-transform:uppercase;">Input Data</div>`;
html += `<div class="payload-box">`; html += `<div class="payload-box">`;
params.forEach(p => { 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) : ""; const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : "";
html += `<div class="payload-param"> html += `<div class="payload-param">
<span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span> <span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span>

View File

@@ -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/

View File

@@ -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/

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>click.kamil.complex</groupId>
<artifactId>complex-multi-module-sm</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>domain</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,24 @@
package click.kamil.domain;
import org.springframework.messaging.support.GenericMessage;
import java.util.Map;
public class CustomStateMachineMessage<T, P> extends GenericMessage<T> {
private final P customPayload;
public CustomStateMachineMessage(T payload, P customPayload) {
super(payload);
this.customPayload = customPayload;
}
public CustomStateMachineMessage(T payload, Map<String, Object> headers, P customPayload) {
super(payload, headers);
this.customPayload = customPayload;
}
public P getCustomPayload() {
return customPayload;
}
}

View File

@@ -0,0 +1,4 @@
package click.kamil.domain;
public interface IEvent {
}

View File

@@ -0,0 +1,4 @@
package click.kamil.domain;
public interface IState {
}

View File

@@ -0,0 +1,5 @@
package click.kamil.domain;
public enum OrderEvent implements IEvent {
PROCESS, COMPLETE, CANCEL
}

View File

@@ -0,0 +1,5 @@
package click.kamil.domain;
public enum OrderState implements IState {
NEW, PROCESSING, COMPLETED, CANCELLED
}

View File

@@ -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<OrderState, OrderEvent> {
@Override
public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
states
.withStates()
.initial(OrderState.NEW)
.states(EnumSet.allOf(OrderState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> 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);
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>click.kamil.complex</groupId>
<artifactId>complex-multi-module-sm</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>domain</module>
<module>service</module>
<module>web</module>
</modules>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<spring-boot.version>3.2.0</spring-boot.version>
<spring-statemachine.version>3.2.0</spring-statemachine.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>${spring-statemachine.version}</version>
</dependency>
<dependency>
<groupId>click.kamil.complex</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>click.kamil.complex</groupId>
<artifactId>service</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@@ -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/

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>click.kamil.complex</groupId>
<artifactId>complex-multi-module-sm</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>service</artifactId>
<dependencies>
<dependency>
<groupId>click.kamil.complex</groupId>
<artifactId>domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,7 @@
package click.kamil.service;
import click.kamil.domain.IEvent;
public interface OutboxAction<T extends IEvent> {
void onCommit(T event);
}

View File

@@ -0,0 +1,22 @@
package click.kamil.service;
import click.kamil.domain.IEvent;
import click.kamil.domain.IState;
public interface StateMachineService {
<S extends IState, T extends IEvent> void sendMessage(T event);
<S extends IState, T extends IEvent, A extends OutboxAction<T>> void sendMessageWithOutbox(T event, A action);
// Quirky pattern: Bounded generic supplier instead of reflection
<T extends click.kamil.domain.OrderEvent> void sendMessageWithProvider(java.util.function.Supplier<T> eventProvider);
// Quirky pattern 2: Generic Varargs
<T extends IEvent> void sendEventsVarargs(T... events);
// Child class of Spring's GenericMessage with a generic payload
<T extends click.kamil.domain.OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage);
// Quirky pattern 3: Multiple generic bounds (Intersection Types)
<T extends IEvent & java.io.Serializable & Cloneable> T processQuirkyEvent(T event);
}

View File

@@ -0,0 +1,78 @@
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<OrderState, OrderEvent> stateMachine;
@Override
public <S extends IState, T extends IEvent> void sendMessage(T event) {
if (event instanceof OrderEvent) {
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload((OrderEvent) event).build())).subscribe();
}
}
@Override
@Transactional
public <S extends IState, T extends IEvent, A extends OutboxAction<T>> 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 <T extends OrderEvent> void sendMessageWithProvider(java.util.function.Supplier<T> eventProvider) {
T event = eventProvider.get();
if (event != null) {
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload((OrderEvent) event).build())).subscribe();
}
}
@Override
@SafeVarargs
public final <T extends IEvent> void sendEventsVarargs(T... events) {
for (T event : events) {
sendMessage(event);
}
}
@Override
public <T extends OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage) {
// Because CustomStateMachineMessage extends GenericMessage<T> and implements Message<T>,
// we can pass it directly to the state machine as long as T resolves to OrderEvent!
// We cast it to Message<OrderEvent> to satisfy Mono's strict type requirements
org.springframework.messaging.Message<OrderEvent> msg = (org.springframework.messaging.Message<OrderEvent>) customMessage;
stateMachine.sendEvent(Mono.just(msg)).subscribe();
}
@Override
public <T extends IEvent & java.io.Serializable & Cloneable> 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
}
}

View File

@@ -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/

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>click.kamil.complex</groupId>
<artifactId>complex-multi-module-sm</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>web</artifactId>
<dependencies>
<dependency>
<groupId>click.kamil.complex</groupId>
<artifactId>service</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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";
}
}

View File

@@ -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<OrderEvent, String> 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");
}
}