15 Commits

Author SHA1 Message Date
d59f4ac166 fix attempt 1 2026-06-20 07:04:44 +02:00
19547d3c5f better html tooltip styling 2026-06-20 06:16:12 +02:00
ab37eb5d40 call graph fix + tests 2026-06-20 05:51:42 +02:00
bf9208d529 better enricher attempt 2026-06-19 21:21:44 +02:00
7077214c81 better enricher 2026-06-19 20:56:14 +02:00
344e295106 enricher 2026-06-19 19:18:23 +02:00
d6b1571f18 test1 2026-06-19 17:45:57 +02:00
e617fb1754 update 1 test 2026-06-19 17:37:08 +02:00
06dc0ba641 update 1 2026-06-19 17:33:32 +02:00
ba412b905e moar 2026-06-19 17:25:01 +02:00
198c5d830e test 2026-06-19 17:23:27 +02:00
a383de5a5f fix html attempt 2 2026-06-19 17:19:26 +02:00
ef9ebe3512 fix html 2026-06-19 17:11:34 +02:00
b480dc83ef transition enricher rabbit 2026-06-19 09:22:27 +02:00
0a23daae05 transition enricher 2026-06-19 09:06:37 +02:00
60 changed files with 3608 additions and 200 deletions

View File

@@ -1,10 +1,9 @@
1.extract from all possible formats (ask chatgpt)
[DONE] json
[TODO] xml, maybe yaml
2. [PLAN] Support external dependencies (JARs/compiled classes):
- Enable binding resolution in ASTParser (`setResolveBindings(true)`).
- Configure ASTParser with project classpath (including JARs).
- Refactor `CodebaseContext` to handle resolved bindings, not just local AST nodes.
- Implement tests using external libraries as base classes.
- Implement tests using external libraries as base classes.

View File

@@ -36,6 +36,7 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
compileOnly 'org.projectlombok:lombok:1.18.46'
annotationProcessor 'org.projectlombok:lombok:1.18.46'

View File

@@ -14,6 +14,14 @@ import java.util.List;
public class Main {
public static void main(String[] args) {
// Enable diagnostic mode early before SLF4J initializes
for (String arg : args) {
if ("--debug".equals(arg)) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
break;
}
}
// Manual DI / Wiring
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
var exportService = new ExportService(exporters);

View File

@@ -41,20 +41,62 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
if (t.getEvent() != null) {
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
String smEvent = simplify(smEventRaw);
if (smEvent.equals(triggerEvent)) {
// Event matches. Check source state if provided
boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") ||
triggerEvent.equals("msg") || triggerEvent.equals("message") ||
triggerEvent.equals("payload");
if (isWildcard) {
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
// We no longer hard-block wildcards without a specific routing context.
// If a project doesn't use standard SM persisters (e.g. restores state manually),
// contextMachineId will be null. We should still link the wildcard to provide SOME visibility,
// rather than completely hiding the endpoint.
}
List<String> polyEvents = tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : java.util.Collections.emptyList();
boolean hasPolyMatch = false;
for (String pe : polyEvents) {
String simplePe = pe;
if (pe.contains(".")) {
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
}
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
hasPolyMatch = true;
break;
}
}
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())))) {
// Event matches or is a wildcard
for (State smSourceState : t.getSourceStates()) {
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
String smSource = simplify(smSourceRaw);
if (triggerSource == null || triggerSource.equals(smSource)) {
for (State smTargetState : t.getTargetStates()) {
String sourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
matched.add(MatchedTransition.builder()
.sourceState(sourceRaw)
.targetState(targetRaw)
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
MatchedTransition mt = MatchedTransition.builder()
.sourceState(smSourceRaw)
.targetState(smSourceRaw)
.event(smEventRaw)
.build());
.build();
if (isRoutedToCorrectMachine(chain, result.getName())) {
matched.add(mt);
}
} else {
for (State smTargetState : t.getTargetStates()) {
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
MatchedTransition mt = MatchedTransition.builder()
.sourceState(smSourceRaw)
.targetState(targetRaw)
.event(smEventRaw)
.build();
if (isRoutedToCorrectMachine(chain, result.getName())) {
matched.add(mt);
}
}
}
}
}
@@ -62,16 +104,12 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
}
}
// Create a new CallChain with the matched transitions
CallChain updatedChain = CallChain.builder()
.entryPoint(chain.getEntryPoint())
.methodChain(chain.getMethodChain())
.triggerPoint(chain.getTriggerPoint())
.contextMachineId(chain.getContextMachineId())
.matchedTransitions(matched)
.build();
updatedChains.add(updatedChain);
if (!matched.isEmpty()) {
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
updatedChains.add(newChain);
} else {
updatedChains.add(chain);
}
}
// Update the metadata with the new call chains
@@ -85,12 +123,38 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
result.setMetadata(updatedMetadata);
}
private String simplify(String name) {
if (name == null) return "";
int dot = name.lastIndexOf('.');
if (dot >= 0) {
return name.substring(dot + 1);
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
// If the chain's target expression or qualifier indicates a specific machine name, verify it
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
return name;
if (targetVar != null && !targetVar.isEmpty()) {
targetVar = targetVar.toLowerCase();
// E.g., if target is "myStateMachine", ensure current machine name contains "my"
String simplifiedMachineName = currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase();
// If the variable name ends with StateMachine, we extract the prefix
if (targetVar.endsWith("statemachine")) {
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
if (!prefix.isEmpty() && !simplifiedMachineName.contains(prefix)) {
return false;
}
}
}
return true;
}
private String simplify(String name) {
if (name == null) return null;
// Strip common suffixes
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
if (simplified.isEmpty()) {
simplified = name;
}
// Simplify full identifiers to just the last part (enum name)
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
return simplified;
}
}

View File

@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
import java.util.List;
@Data
@Builder
@Builder(toBuilder = true)
@Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true)
public class CallChain {

View File

@@ -13,5 +13,7 @@ import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class LibraryHint {
private final String methodFqn; // e.g., "com.thirdparty.Workflow.send"
private final String event; // The event it triggers
private final String event; // The event it triggers (static)
private final Integer eventArgumentIndex; // e.g., 0 to extract from the 0th argument
private final String eventArgumentMethod; // e.g., "getType" to extract from argument method call
}

View File

@@ -20,4 +20,5 @@ public class TriggerPoint {
private final String stateMachineId; // Optional: to link to a specific SM instance
private final String sourceState; // Optional: if we can determine the expected current state
private final int lineNumber;
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
}

View File

@@ -48,12 +48,37 @@ public class ConstantResolver {
return resolveInfix(infix, context, visited);
}
if (expr instanceof QualifiedName qn) {
return resolveManual(qn, context, visited);
String val = resolveManual(qn, context, visited);
return val != null ? val : qn.toString();
}
if (expr instanceof SimpleName sn) {
return resolveManual(sn, context, visited);
}
if (expr instanceof MethodInvocation mi) {
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
ITypeBinding returnType = mb.getReturnType();
if (returnType != null) {
java.util.List<String> values = context.getEnumValues(returnType.getQualifiedName());
if (values != null && !values.isEmpty()) {
return "ENUM_SET:" + String.join(",", values);
}
}
} else {
TypeDeclaration td = findEnclosingType(mi);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
if (md != null && md.getReturnType2() != null) {
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
if (values != null && !values.isEmpty()) {
return "ENUM_SET:" + String.join(",", values);
}
}
}
}
}
return null;
}

View File

@@ -105,25 +105,32 @@ public class PropertyResolver {
}
private Map<String, String> loadYaml(Path path) {
// Placeholder for future YAML support
log.warn("YAML parsing not fully implemented yet for {}", path);
return new HashMap<>();
Map<String, String> props = new HashMap<>();
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(new com.fasterxml.jackson.dataformat.yaml.YAMLFactory());
Map<String, Object> map = mapper.readValue(path.toFile(), new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
flattenYaml("", map, props);
} catch (IOException e) {
log.warn("Failed to load YAML from {}", path);
}
return props;
}
@SuppressWarnings("unchecked")
private void flattenYaml(String prefix, Map<String, Object> map, Map<String, String> result) {
if (map == null) return;
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
flattenYaml(key, (Map<String, Object>) value, result);
} else if (value != null) {
result.put(key, value.toString());
}
}
}
public String resolveValue(String placeholder, Map<String, String> properties) {
if (placeholder == null || !placeholder.contains("${")) return placeholder;
// Very basic placeholder extraction: ${key} or ${key:default}
String content = placeholder.substring(placeholder.indexOf("${") + 2, placeholder.lastIndexOf("}"));
String key = content;
String defaultValue = null;
if (content.contains(":")) {
int colonIndex = content.indexOf(":");
key = content.substring(0, colonIndex);
defaultValue = content.substring(colonIndex + 1);
}
return properties.getOrDefault(key, defaultValue);
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(placeholder, properties);
}
}

View File

@@ -34,10 +34,12 @@ public class CallGraphBuilder {
for (EntryPoint ep : entryPoints) {
String startMethod = ep.getClassName() + "." + ep.getMethodName();
boolean foundAny = false;
for (TriggerPoint tp : triggers) {
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
if (path != null) {
foundAny = true;
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
String contextMachineId = extractContextMachineId(path, callGraph);
chains.add(CallChain.builder()
@@ -48,6 +50,9 @@ public class CallGraphBuilder {
.build());
}
}
if (!foundAny && log.isDebugEnabled()) {
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
}
}
return chains;
}
@@ -58,47 +63,342 @@ 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;
String currentParamName = event;
String resolvedValue = event;
String methodSuffix = "";
// Extract method calls like .getType() so we can trace the base parameter
int dotIndex = currentParamName.indexOf('.');
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
char nextChar = currentParamName.charAt(dotIndex + 1);
if (Character.isLowerCase(nextChar)) {
methodSuffix = currentParamName.substring(dotIndex);
currentParamName = currentParamName.substring(0, dotIndex);
}
}
// 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) {
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
String tracedVar = traceLocalVariable(target, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
// Extract method calls like .getType() from the traced variable
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
paramIndex = getParameterIndex(target, currentParamName);
}
if (paramIndex >= 0) {
String caller = path.get(path.size() - 2);
String target = path.get(path.size() - 1);
List<CallEdge> 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();
}
}
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<CallEdge> 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) {
// If the argument passed has a method call, extract it
int dotIdx = arg.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
methodSuffix = arg.substring(dotIdx) + methodSuffix;
arg = arg.substring(0, dotIdx);
}
currentParamName = arg;
resolvedValue = arg + methodSuffix;
found = true;
break;
}
}
}
}
}
if (!found) break; // Could not map argument
}
// Final check on the entry method
String entryMethod = path.get(0);
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
if (entryParamIndex < 0) {
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
}
}
List<String> 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;
}
String methodName = resolvedValue.substring(lastDot + 1, openParen);
if (varName != null) {
// Resolve in the first method in the path where the variable might be declared
for (String methodFqn : path) {
String declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType);
List<String> typesToInspect = new ArrayList<>();
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
for (String type : typesToInspect) {
Set<String> visited = new HashSet<>();
// We must find the compilation unit to pass for simple names!
// Let's pass the first available CU for this class
TypeDeclaration baseTd = context.getTypeDeclaration(type);
CompilationUnit cuToUse = null;
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) baseTd.getRoot();
} else {
String entryClassName = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) entryTd.getRoot();
}
}
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
for (String constant : constants) {
if (!polymorphicEvents.contains(constant)) {
polymorphicEvents.add(constant);
}
}
}
break;
}
}
}
}
}
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.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 getVariableDeclaredType(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
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 (Object pObj : md.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
if (svd.getName().getIdentifier().equals(varName)) {
return svd.getType().toString();
}
}
final String[] foundType = new String[1];
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType().toString();
}
}
return super.visit(node);
}
});
}
return foundType[0];
}
}
return null;
}
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
if (depth > 20) return Collections.emptyList();
String fqn = className + "." + methodName;
if (!visited.add(fqn)) return Collections.emptyList();
List<String> constants = new ArrayList<>();
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
if (td == null) {
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
}
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
Expression retExpr = node.getExpression();
if (retExpr != null) {
boolean handled = false;
if (retExpr instanceof MethodInvocation mi) {
// Follow delegation first
String called = resolveCalledMethod(mi);
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
if (called != null && called.contains(".")) {
if (visited.contains(called)) {
handled = true;
} else {
String cName = called.substring(0, called.lastIndexOf('.'));
String mName = called.substring(called.lastIndexOf('.') + 1);
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
}
if (!handled) {
String val = constantResolver.resolve(retExpr, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
constants.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
constants.add(val);
}
} else if (retExpr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (retExpr instanceof SimpleName sn) {
constants.add(sn.toString());
}
}
}
return super.visit(node);
}
});
}
}
visited.remove(fqn);
return constants;
}
private String traceLocalVariable(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
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 && md.getBody() != null) {
final Expression[] initializer = new Expression[1];
md.getBody().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) {
Expression expr = traceVariable(initializer[0]);
if (expr instanceof MethodInvocation mi) {
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
Expression innerMost = unwrapMethodInvocation(mi, 0);
if (innerMost instanceof MethodInvocation innerMi) {
if (innerMi.getExpression() instanceof SimpleName sn) {
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
}
return innerMi.getName().getIdentifier() + "()";
}
if (innerMost instanceof SimpleName sn) {
return sn.getIdentifier();
}
return innerMost.toString();
}
if (expr instanceof SimpleName sn) {
return sn.getIdentifier();
}
return expr.toString();
}
}
}
return null;
}
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
if (depth > 5) return mi;
if (!mi.arguments().isEmpty()) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1);
}
return arg;
}
return mi;
}
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
for (String node : path) {
List<CallEdge> edges = callGraph.get(node);
@@ -117,26 +417,62 @@ public class CallGraphBuilder {
return null;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
return (MethodDeclaration) parent;
}
private Map<String, List<CallEdge>> buildCallGraph() {
graph = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
currentMethodFqn = context.getFqn(td) + "." + node.getName().getIdentifier();
}
return super.visit(node);
}
@Override
public boolean visit(MethodInvocation node) {
if (currentMethodFqn != null) {
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration td = findEnclosingType(md);
if (td != null) {
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
String typeName = emr.getExpression().toString();
if ("this".equals(typeName) || "super".equals(typeName)) {
TypeDeclaration td2 = findEnclosingType(node);
if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
}
}
} else {
String fallbackTypeFqn = null;
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
if (binding != null) {
fallbackTypeFqn = binding.getQualifiedName();
} else if (emr.getExpression() instanceof SimpleName sn) {
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
}
if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
}
}
}
}
}
}
}
return super.visit(node);
@@ -144,22 +480,27 @@ public class CallGraphBuilder {
@Override
public boolean visit(SuperMethodInvocation node) {
if (currentMethodFqn != null) {
String methodName = node.getName().getIdentifier();
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
String calledMethod = null;
if (superTd != null) {
calledMethod = resolveMethodInType(superTd, methodName);
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration tdOuter = findEnclosingType(md);
if (tdOuter != null) {
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
String methodName = node.getName().getIdentifier();
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
String calledMethod = null;
if (superTd != null) {
calledMethod = resolveMethodInType(superTd, methodName);
}
if (calledMethod == null) {
calledMethod = superFqn + "." + methodName;
}
List<String> args = resolveArguments(node.arguments());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
if (calledMethod == null) {
calledMethod = superFqn + "." + methodName;
}
List<String> args = resolveArguments(node.arguments());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
}
}
@@ -174,6 +515,51 @@ public class CallGraphBuilder {
List<String> 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;
}
}
}
}
if (expr instanceof ExpressionMethodReference emr) {
TypeDeclaration td = findEnclosingType(expr);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
}
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
@@ -220,7 +606,18 @@ public class CallGraphBuilder {
return binding.getQualifiedName() + "." + methodName;
}
if (receiver instanceof ThisExpression) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return context.getFqn(td) + "." + methodName;
}
}
if (receiver instanceof SimpleName sn) {
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
if (fallbackTypeFqn != null) {
return fallbackTypeFqn + "." + methodName;
}
String receiverName = sn.getIdentifier();
return receiverName + "." + methodName;
}
@@ -228,6 +625,85 @@ public class CallGraphBuilder {
return null;
}
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
String varName = receiverNameNode.getIdentifier();
// 1. Check local variables in enclosing method
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
if (enclosingMethod != null) {
// Check parameters
for (Object paramObj : enclosingMethod.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
if (svd.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(svd.getType(), receiverNameNode);
}
}
// Check method body (local variables)
if (enclosingMethod.getBody() != null) {
Type[] foundType = new Type[1];
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType();
}
}
return super.visit(node);
}
});
if (foundType[0] != null) {
return resolveTypeToFqn(foundType[0], receiverNameNode);
}
}
}
// 2. Check fields in enclosing class
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
if (enclosingType != null) {
for (FieldDeclaration field : enclosingType.getFields()) {
for (Object fragObj : field.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(field.getType(), receiverNameNode);
}
}
}
}
return null;
}
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
} else {
simpleName = type.toString();
}
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
return simpleName;
}
private String resolveMethodInType(TypeDeclaration td, String methodName) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
@@ -257,6 +733,11 @@ public class CallGraphBuilder {
}
}
}
if (log.isDebugEnabled()) {
log.debug("Path search dead-end at {} when looking for {}", start, target);
}
visited.remove(start);
return null;
}
@@ -275,4 +756,33 @@ 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;
}
}

View File

@@ -47,10 +47,12 @@ public class GenericEventDetector {
}
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
TriggerPoint trigger = buildTriggerPoint(node, cu, null);
if (trigger != null) {
log.debug("Successfully built trigger point: {}", trigger.getEvent());
triggers.add(trigger);
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
if (builtTriggers != null) {
for (TriggerPoint trigger : builtTriggers) {
log.debug("Successfully built trigger point: {}", trigger.getEvent());
triggers.add(trigger);
}
}
}
@@ -62,10 +64,23 @@ public class GenericEventDetector {
for (LibraryHint hint : hints) {
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
TriggerPoint trigger = buildTriggerPoint(node, cu, hint.getEvent());
if (trigger != null) {
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
triggers.add(trigger);
String eventToUse = hint.getEvent();
if (eventToUse == null && hint.getEventArgumentIndex() != null) {
if (node.arguments().size() > hint.getEventArgumentIndex()) {
Expression argExpr = (Expression) node.arguments().get(hint.getEventArgumentIndex());
eventToUse = argExpr.toString();
if (hint.getEventArgumentMethod() != null && !hint.getEventArgumentMethod().isEmpty()) {
eventToUse = eventToUse + "." + hint.getEventArgumentMethod() + "()";
}
}
}
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, eventToUse);
if (builtTriggers != null) {
for (TriggerPoint trigger : builtTriggers) {
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
triggers.add(trigger);
}
}
}
}
@@ -102,12 +117,12 @@ public class GenericEventDetector {
return receiver.toString() + "." + methodName;
}
private TriggerPoint buildTriggerPoint(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
private List<TriggerPoint> buildTriggerPoints(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
String eventValue = null;
if (forcedEvent != null) {
eventValue = context.resolveString(forcedEvent);
} else {
if (node.arguments().isEmpty()) return null;
if (node.arguments().isEmpty()) return Collections.emptyList();
Expression eventExpr = (Expression) node.arguments().get(0);
// Try MessageBuilder extraction first
@@ -123,18 +138,37 @@ public class GenericEventDetector {
MethodDeclaration method = findEnclosingMethod(node);
TypeDeclaration type = findEnclosingType(node);
if (type == null) return null;
if (type == null) return Collections.emptyList();
String sourceState = extractSourceState(node);
List<TriggerPoint> results = new ArrayList<>();
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
String[] parts = eventValue.substring(9).split(",");
for (String part : parts) {
if (!part.trim().isEmpty()) {
results.add(TriggerPoint.builder()
.event(part.trim())
.sourceState(sourceState)
.className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition()))
.build());
}
}
} else {
results.add(TriggerPoint.builder()
.event(eventValue)
.sourceState(sourceState)
.className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition()))
.build());
}
return TriggerPoint.builder()
.event(eventValue)
.sourceState(sourceState)
.className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition()))
.build();
return results;
}
private String extractSourceState(ASTNode node) {
@@ -193,6 +227,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 +256,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;
}
}
}
}
}
@@ -240,10 +287,23 @@ 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
String traced = extractEventFromMessageBuilder(sn);
return traced != null ? traced : sn.getIdentifier();
}
Expression receiver = current.getExpression();

View File

@@ -24,6 +24,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
private final CallGraphBuilder callGraphBuilder;
private final LifecycleDetector lifecycleDetector;
private final InterceptorDetector interceptorDetector;
private final SpringComponentDetector componentDetector;
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
this.context = context;
@@ -34,6 +35,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
this.callGraphBuilder = new CallGraphBuilder(context);
this.lifecycleDetector = new LifecycleDetector(context);
this.interceptorDetector = new InterceptorDetector(context);
this.componentDetector = new SpringComponentDetector(context);
}
@Override
@@ -58,6 +60,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
allEntryPoints.addAll(mvcDetector.detect(cu));
allEntryPoints.addAll(messagingDetector.detect(cu));
allEntryPoints.addAll(interceptorDetector.detect(cu));
allEntryPoints.addAll(componentDetector.detect(cu));
}
log.info("Found {} entry points (including interceptors and listeners) in total", allEntryPoints.size());
return allEntryPoints;

View File

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

View File

@@ -0,0 +1,117 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.RequiredArgsConstructor;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
public class SpringComponentDetector {
private final CodebaseContext context;
public List<EntryPoint> detect(CompilationUnit cu) {
List<EntryPoint> entryPoints = new ArrayList<>();
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
if (!(node.getParent() instanceof TypeDeclaration)) {
return super.visit(node);
}
TypeDeclaration parentTd = (TypeDeclaration) node.getParent();
for (Object modifier : node.modifiers()) {
if (modifier instanceof Annotation annotation) {
String typeName = annotation.getTypeName().getFullyQualifiedName();
if (typeName.endsWith("Scheduled")) {
Map<String, String> meta = new HashMap<>();
String cron = extractAnnotationValue(annotation, "cron");
if (!cron.isEmpty())
meta.put("cron", cron);
String fixedRate = extractAnnotationValue(annotation, "fixedRate");
if (!fixedRate.isEmpty())
meta.put("fixedRate", fixedRate);
String fixedDelay = extractAnnotationValue(annotation, "fixedDelay");
if (!fixedDelay.isEmpty())
meta.put("fixedDelay", fixedDelay);
entryPoints.add(EntryPoint.builder()
.type(EntryPoint.Type.CUSTOM)
.name("@Scheduled: " + node.getName().getIdentifier())
.className(context.getFqn(parentTd))
.methodName(node.getName().getIdentifier())
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
.metadata(meta)
.build());
} else if (typeName.endsWith("EventListener")) {
Map<String, String> meta = new HashMap<>();
String classes = extractAnnotationValue(annotation, "classes");
if (!classes.isEmpty())
meta.put("classes", classes);
String condition = extractAnnotationValue(annotation, "condition");
if (!condition.isEmpty())
meta.put("condition", condition);
entryPoints.add(EntryPoint.builder()
.type(EntryPoint.Type.CUSTOM)
.name("@EventListener: " + node.getName().getIdentifier())
.className(context.getFqn(parentTd))
.methodName(node.getName().getIdentifier())
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
.metadata(meta)
.build());
} else if (typeName.endsWith("Around") || typeName.endsWith("Before") || typeName.endsWith("After") || typeName.endsWith("AfterReturning") || typeName.endsWith("AfterThrowing")) {
Map<String, String> meta = new HashMap<>();
String value = extractAnnotationValue(annotation, "value");
if (!value.isEmpty())
meta.put("pointcut", value);
else {
String pointcut = extractAnnotationValue(annotation, "pointcut");
if (!pointcut.isEmpty())
meta.put("pointcut", pointcut);
}
entryPoints.add(EntryPoint.builder()
.type(EntryPoint.Type.CUSTOM)
.name("@" + annotation.getTypeName().getFullyQualifiedName() + ": " + node.getName().getIdentifier())
.className(context.getFqn(parentTd))
.methodName(node.getName().getIdentifier())
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
.metadata(meta)
.build());
}
}
}
return super.visit(node);
}
});
return entryPoints;
}
private String extractAnnotationValue(Annotation annotation, String memberName) {
if (annotation instanceof SingleMemberAnnotation sma) {
if ("value".equals(memberName)) {
return sma.getValue().toString();
}
} else if (annotation instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if (pair.getName().getIdentifier().equals(memberName)) {
return pair.getValue().toString();
}
}
}
return "";
}
}

View File

@@ -170,6 +170,7 @@ public class CodebaseContext {
private final List<CompilationUnit> allCus = new ArrayList<>();
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
private final Map<String, List<String>> enumValues = new HashMap<>();
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
@@ -188,6 +189,8 @@ public class CodebaseContext {
for (Object type : cu.types()) {
if (type instanceof TypeDeclaration td) {
indexType(td, packageName, javaFile);
} else if (type instanceof EnumDeclaration ed) {
indexEnum(ed, packageName, javaFile);
}
}
}
@@ -210,9 +213,18 @@ public class CodebaseContext {
}
// Inheritance Mapping
for (Object itf : td.superInterfaceTypes()) {
String itfName = itf.toString();
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
// Track implementations (for both interfaces and superclasses)
if (td.superInterfaceTypes() != null) {
for (Object itfObj : td.superInterfaceTypes()) {
Type itf = (Type) itfObj;
String itfName = extractTypeName(itf);
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
}
}
Type superclass = td.getSuperclassType();
if (superclass != null) {
String superName = extractTypeName(superclass);
interfaceToImpls.computeIfAbsent(superName, k -> new ArrayList<>()).add(fqn);
}
// Recursively index nested types
@@ -224,15 +236,67 @@ public class CodebaseContext {
}
public List<String> getImplementations(String interfaceName) {
Set<String> allImpls = new HashSet<>();
collectImplementations(interfaceName, allImpls, new HashSet<>());
return new ArrayList<>(allImpls);
}
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
if (!visited.add(typeName)) return;
// Try direct match
List<String> impls = interfaceToImpls.get(interfaceName);
if (impls != null) return impls;
List<String> directImpls = interfaceToImpls.get(typeName);
// Try FQN match if input was simple name
String fqn = simpleNameToFqn.get(interfaceName);
if (fqn != null) return interfaceToImpls.getOrDefault(fqn, Collections.emptyList());
if (directImpls == null) {
String fqn = simpleNameToFqn.get(typeName);
if (fqn != null) {
directImpls = interfaceToImpls.get(fqn);
}
}
return Collections.emptyList();
// Try simple name match if input was FQN
if (directImpls == null && typeName.contains(".")) {
String simpleName = typeName.substring(typeName.lastIndexOf('.') + 1);
directImpls = interfaceToImpls.get(simpleName);
}
if (directImpls != null) {
for (String impl : directImpls) {
results.add(impl);
// Recursively find implementations of the implementation (e.g., subclasses of an abstract class)
collectImplementations(impl, results, visited);
}
}
}
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
String simpleName = ed.getName().getIdentifier();
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
classes.put(fqn, (CompilationUnit) ed.getRoot());
classPaths.put(fqn, javaFile);
List<String> values = new ArrayList<>();
for (Object o : ed.enumConstants()) {
if (o instanceof EnumConstantDeclaration ecd) {
values.add(fqn + "." + ecd.getName().getIdentifier());
}
}
enumValues.put(fqn, values);
if (!simpleNameToFqn.containsKey(simpleName)) {
simpleNameToFqn.put(simpleName, fqn);
}
}
public List<String> getEnumValues(String fqnOrSimpleName) {
if (fqnOrSimpleName == null) return null;
List<String> values = enumValues.get(fqnOrSimpleName);
if (values != null) return values;
String fqn = simpleNameToFqn.get(fqnOrSimpleName);
if (fqn != null) return enumValues.get(fqn);
// Also try stripping array or generic types if any (e.g. MyEnum[])
return null;
}
public State resolveState(Expression expr, CompilationUnit cu) {

View File

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

View File

@@ -54,11 +54,19 @@ public class ExporterCommand implements Callable<Integer> {
@Option(names = {"--state"}, description = "Format for states when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
private click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat;
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
private boolean debug;
@Override
public Integer call() throws Exception {
var out = spec.commandLine().getOut();
var err = spec.commandLine().getErr();
if (debug) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,cyan Diagnostic mode enabled|@"));
}
if (inputDir == null && jsonFile == null) {
inputDir = Path.of(".");
}

View File

@@ -124,9 +124,13 @@ public class PlantUml implements StateMachineExporter {
sb.append(" : [[#").append(linkId).append(" ");
if (!label.isEmpty()) {
sb.append(label);
String sanitized = label.replace("[", "&#91;")
.replace("]", "&#93;")
.replace("<", "&lt;")
.replace(">", "&gt;");
sb.append(sanitized);
} else {
sb.append("<U+00A0>");
sb.append(".");
}
sb.append("]]");
} else if (!label.isEmpty()) {

View File

@@ -23,8 +23,7 @@ import java.util.List;
public class GoldenUpdater {
private static final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
private static final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
private static final ExportService exportService = new ExportService(exporters, enrichmentService);
private static final ExportService exportService = new ExportService(exporters);
public static void main(String[] args) throws Exception {
List<TestScenario> scenarios = provideTestScenarios();

View File

@@ -137,6 +137,12 @@ public class RegressionTest {
root.resolve("state_machines/maven_multi_module/core-module"),
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
"MavenOrderStateMachine"
),
new TestScenario(
"Complex Multi-Module Sample",
root.resolve("state_machines/complex_multi_module_sm"),
Path.of("src/test/resources/golden/StateMachineConfig"),
"StateMachineConfig"
)
);
}

View File

@@ -118,4 +118,78 @@ class TransitionLinkerEnricherTest {
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNull();
}
@Test
void shouldMatchWildcardVariable() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("event").build())
.contextMachineId("testMachine")
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
@Test
void shouldNotMatchArbitraryGetXMethodWildcard() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("richEvent.getId()").build())
.contextMachineId("testMachine")
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNull();
}
@Test
void shouldAllowWildcardWhenContextMachineIdIsMissing() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("event").build())
// NO contextMachineId or stateMachineId
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
}

View File

@@ -0,0 +1,60 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class PropertyResolverTest {
@Test
void testYamlPropertiesAreLoadedAndFlattened(@TempDir Path tempDir) throws IOException {
Path applicationYml = tempDir.resolve("application.yml");
Files.writeString(applicationYml, """
messaging:
sqs:
integration-system1:
callback:
queue: "integration-system1-queue"
""");
PropertyResolver resolver = new PropertyResolver();
Map<String, String> props = resolver.resolveProperties(tempDir);
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
}
@Test
void testPropertiesFilesAreLoaded(@TempDir Path tempDir) throws IOException {
Path applicationProps = tempDir.resolve("application.properties");
Files.writeString(applicationProps, "messaging.sqs.integration-system1.callback.queue=integration-system1-queue-props");
PropertyResolver resolver = new PropertyResolver();
Map<String, String> props = resolver.resolveProperties(tempDir);
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue-props");
}
@Test
void testPlaceholderResolution() {
PropertyResolver resolver = new PropertyResolver();
Map<String, String> properties = Map.of("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.integration-system1.callback.queue}", properties);
assertThat(resolved).isEqualTo("SQS: integration-system1-queue");
}
@Test
void testPlaceholderResolutionWithDefault() {
PropertyResolver resolver = new PropertyResolver();
Map<String, String> properties = Map.of();
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.queue:default-queue}", properties);
assertThat(resolved).isEqualTo("SQS: default-queue");
}
}

View File

@@ -44,4 +44,71 @@ public class GenericEventDetectorTest {
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
}
@Test
void testMessageBuilderIntermediateVariable(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("MyService.java"),
"package com.example;\n" +
"import org.springframework.messaging.support.MessageBuilder;\n" +
"import org.springframework.messaging.Message;\n" +
"import org.springframework.statemachine.StateMachine;\n" +
"public class MyService {\n" +
" private StateMachine<String, String> stateMachine;\n" +
" private void a(MyInterface myEvent) {\n" +
" MessageBuilder<MyInterface> messageBuilder = MessageBuilder.withPayload(myEvent).setHeader(\"k\", \"v\");\n" +
" stateMachine.sendEvent(messageBuilder.build());\n" +
" }\n" +
"}\n");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
CompilationUnit cu = context.getCompilationUnits().iterator().next();
List<TriggerPoint> triggers = detector.detect(cu);
assertThat(triggers).hasSize(1);
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
}
@Test
void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("MyEnum.java"),
"package com.example;\n" +
"public enum MyEnum {\n" +
" STATE_A, STATE_B;\n" +
"}\n");
Files.writeString(dir.resolve("MyService.java"),
"package com.example;\n" +
"import org.springframework.statemachine.StateMachine;\n" +
"public class MyService {\n" +
" private StateMachine<String, String> stateMachine;\n" +
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
" public void trigger() {\n" +
" stateMachine.sendEvent(this.getEvent());\n" +
" }\n" +
"}\n");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
CompilationUnit serviceCu = context.getCompilationUnits().stream()
.filter(cu -> cu.getJavaElement() == null || cu.getJavaElement().getElementName().contains("MyService"))
.findFirst().orElseThrow();
List<TriggerPoint> triggers = detector.detect(serviceCu);
System.out.println("TRIGGERS: " + triggers);
assertThat(triggers).hasSize(2);
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_A"));
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_B"));
}
}

View File

@@ -0,0 +1,55 @@
package click.kamil.springstatemachineexporter.exporter;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.model.Guard;
import click.kamil.springstatemachineexporter.model.Action;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class PlantUmlTest {
@Test
void testExportSanitizesLabels() {
PlantUml plantUml = new PlantUml();
ExportOptions options = ExportOptions.builder()
.embedIdentifiers(true)
.useLambdaGuards(false)
.build();
State state1 = State.of("State1");
State state2 = State.of("State2");
Transition t = new Transition();
t.setType(TransitionType.EXTERNAL);
t.setSourceStates(List.of(state1));
t.setTargetStates(List.of(state2));
// Add a guard and action containing problematic characters
t.setGuard(Guard.of("list.size() < 5", false, null, null, null, null));
t.setActions(List.of(Action.of("doSomething(new int[]{1, 2})", false, null, null, null, null)));
String result = plantUml.export(
"TestMachine",
List.of(t),
Set.of("State1"),
Set.of("State2"),
options
);
// Verify that < and > are replaced with HTML entities
assertThat(result).doesNotContain("< 5");
assertThat(result).contains("&lt; 5");
// Verify that [ and ] are replaced with HTML entities inside the link
assertThat(result).doesNotContain("[list.size() &lt; 5]");
assertThat(result).doesNotContain("new int[]{1, 2}");
assertThat(result).contains("&#91;list.size() &lt; 5&#93;");
assertThat(result).contains("new int&#91;&#93;{1, 2}");
}
}

View File

@@ -8,7 +8,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "PLACE_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
@@ -17,7 +18,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16
"lineNumber" : 16,
"polymorphicEvents" : null
}, {
"event" : "CANCEL_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
@@ -26,7 +28,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21
"lineNumber" : 21,
"polymorphicEvents" : null
}, {
"event" : "PAY_ORDER",
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
@@ -35,7 +38,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
}, {
"event" : "SHIP_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
@@ -44,7 +48,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "RETURN_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
@@ -53,7 +58,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17
"lineNumber" : 17,
"polymorphicEvents" : null
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -137,7 +143,7 @@
"name" : "JMS: shipping.queue",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"sourceFile" : null,
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
"metadata" : {
"protocol" : "JMS",
"destination" : "shipping.queue"
@@ -152,7 +158,7 @@
"name" : "RABBIT: returns.queue",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"sourceFile" : null,
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
"metadata" : {
"protocol" : "RABBIT",
"destination" : "returns.queue"
@@ -185,7 +191,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16
"lineNumber" : 16,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -219,7 +226,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21
"lineNumber" : 21,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -248,10 +256,11 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17
"lineNumber" : 17,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
@@ -278,7 +287,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -292,7 +302,7 @@
"name" : "JMS: shipping.queue",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
"methodName" : "onShippingReady",
"sourceFile" : null,
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
"metadata" : {
"protocol" : "JMS",
"destination" : "shipping.queue"
@@ -312,7 +322,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17
"lineNumber" : 17,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -326,7 +337,7 @@
"name" : "RABBIT: returns.queue",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
"methodName" : "onReturn",
"sourceFile" : null,
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
"metadata" : {
"protocol" : "RABBIT",
"destination" : "returns.queue"
@@ -346,7 +357,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17
"lineNumber" : 17,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -8,7 +8,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34
"lineNumber" : 34,
"polymorphicEvents" : null
}, {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
@@ -17,7 +18,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16
"lineNumber" : 16,
"polymorphicEvents" : null
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -26,7 +28,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -35,7 +38,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20
"lineNumber" : 20,
"polymorphicEvents" : null
}, {
"event" : "CANCEL_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -44,7 +48,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25
"lineNumber" : 25,
"polymorphicEvents" : null
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -53,7 +58,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29
"lineNumber" : 29,
"polymorphicEvents" : null
}, {
"event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -62,7 +68,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -160,7 +167,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19
"lineNumber" : 19,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -190,7 +198,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20
"lineNumber" : 20,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -220,7 +229,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25
"lineNumber" : 25,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -250,7 +260,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34
"lineNumber" : 34,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -284,10 +295,11 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29
"lineNumber" : 29,
"polymorphicEvents" : null
},
"contextMachineId" : "orderId",
"matchedTransitions" : [ ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
@@ -314,7 +326,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -343,7 +356,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16
"lineNumber" : 16,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -8,7 +8,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34
"lineNumber" : 34,
"polymorphicEvents" : null
}, {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
@@ -17,7 +18,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16
"lineNumber" : 16,
"polymorphicEvents" : null
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -26,7 +28,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -35,7 +38,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20
"lineNumber" : 20,
"polymorphicEvents" : null
}, {
"event" : "CANCEL_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -44,7 +48,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25
"lineNumber" : 25,
"polymorphicEvents" : null
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -53,7 +58,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29
"lineNumber" : 29,
"polymorphicEvents" : null
}, {
"event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -62,7 +68,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -160,7 +167,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19
"lineNumber" : 19,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -190,7 +198,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20
"lineNumber" : 20,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -220,7 +229,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25
"lineNumber" : 25,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -250,7 +260,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34
"lineNumber" : 34,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -284,10 +295,11 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29
"lineNumber" : 29,
"polymorphicEvents" : null
},
"contextMachineId" : "orderId",
"matchedTransitions" : [ ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
@@ -314,7 +326,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -343,7 +356,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16
"lineNumber" : 16,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -8,7 +8,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 15
"lineNumber" : 15,
"polymorphicEvents" : null
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -55,7 +56,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 15
"lineNumber" : 15,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -8,7 +8,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 40
"lineNumber" : 40,
"polymorphicEvents" : null
}, {
"event" : "ORDER_EVENT",
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
@@ -17,7 +18,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 52
"lineNumber" : 52,
"polymorphicEvents" : null
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -66,7 +68,7 @@
"name" : "JMS: order.queue",
"className" : "click.kamil.maven.api.JmsOrderListener",
"methodName" : "onMessage",
"sourceFile" : null,
"sourceFile" : "api-module/src/main/java/click/kamil/maven/api/JmsOrderListener.java",
"metadata" : {
"protocol" : "JMS",
"destination" : "order.queue"
@@ -103,7 +105,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 40
"lineNumber" : 40,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -8,7 +8,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -67,7 +68,8 @@
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18
"lineNumber" : 18,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -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 -> NEW;
COMPLETED [fillcolor=lightgray];
CANCELLED [fillcolor=lightgray];
NEW -> PROCESSING [label="OrderEvent.PROCESS", style="solid", color="black"];
PROCESSING -> COMPLETED [label="OrderEvent.COMPLETE", style="solid", color="black"];
NEW -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
PROCESSING -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
}

View File

@@ -0,0 +1,439 @@
{
"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,
"polymorphicEvents" : null
}, {
"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,
"polymorphicEvents" : null
}, {
"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" : 78,
"polymorphicEvents" : null
} ],
"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" : "REST",
"name" : "POST /api/orders/functional-complete",
"className" : "click.kamil.web.OrderController",
"methodName" : "functionalCompleteOrder",
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/functional-complete",
"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,
"polymorphicEvents" : [ ]
},
"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,
"polymorphicEvents" : [ ]
},
"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,
"polymorphicEvents" : [ ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderState.NEW",
"targetState" : "OrderState.CANCELLED",
"event" : "OrderEvent.CANCEL"
}, {
"sourceState" : "OrderState.PROCESSING",
"targetState" : "OrderState.CANCELLED",
"event" : "OrderEvent.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/functional-complete",
"className" : "click.kamil.web.OrderController",
"methodName" : "functionalCompleteOrder",
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/functional-complete",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.web.OrderController.functionalCompleteOrder", "click.kamil.web.OrderController.triggerTransitionFunction", "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,
"polymorphicEvents" : [ ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderState.PROCESSING",
"targetState" : "OrderState.COMPLETED",
"event" : "OrderEvent.COMPLETE"
} ]
}, {
"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" : 78,
"polymorphicEvents" : [ ]
},
"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,
"polymorphicEvents" : [ ]
},
"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,
"polymorphicEvents" : [ ]
},
"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

@@ -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
[*] --> OrderState.NEW
OrderState.NEW -[#1E90FF,bold]-> OrderState.PROCESSING <<external>> : OrderEvent.PROCESS
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.COMPLETED <<external>> : OrderEvent.COMPLETE
OrderState.NEW -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
OrderState.COMPLETED --> [*]
OrderState.CANCELLED --> [*]
@enduml

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
<state id="NEW">
<transition target="PROCESSING" event="OrderEvent.PROCESS"/>
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
</state>
<state id="PROCESSING">
<transition target="COMPLETED" event="OrderEvent.COMPLETE"/>
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
</state>
<state id="COMPLETED">
</state>
<state id="CANCELLED">
</state>
</scxml>

View File

@@ -13,6 +13,14 @@ import java.util.List;
public class Main {
public static void main(String[] args) {
// Enable diagnostic mode early before SLF4J initializes
for (String arg : args) {
if ("--debug".equals(arg)) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
break;
}
}
// Wiring including the specialized HTML exporter
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter(), new HtmlExporter());
var exportService = new ExportService(exporters);

View File

@@ -59,11 +59,19 @@ public class HtmlExporterCommand implements Callable<Integer> {
@Option(names = {"--no-metadata-pane"}, description = "Disable rendering the left metadata pane (entry points, flows) in the HTML viewer.")
private boolean noMetadataPane;
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
private boolean debug;
@Override
public Integer call() throws Exception {
var out = spec.commandLine().getOut();
var err = spec.commandLine().getErr();
if (debug) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,cyan Diagnostic mode enabled|@"));
}
if (inputDir == null && jsonFile == null) {
inputDir = Path.of(".");
}

View File

@@ -238,9 +238,26 @@
.tooltip-content {
padding: 20px;
max-width: 500px;
max-height: 60vh;
overflow-y: auto;
word-break: break-word;
overflow-wrap: anywhere;
}
/* Custom Scrollbar for Tooltips */
.tooltip-content::-webkit-scrollbar {
width: 6px;
}
.tooltip-content::-webkit-scrollbar-track {
background: transparent;
}
.tooltip-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.tooltip-content::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.payload-box {
background: #0f172a;
@@ -576,7 +593,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 +648,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>`;
html += `<div class="payload-box">`;
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 += `<div class="payload-param">
<span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span>

View File

@@ -81,13 +81,6 @@ public class HtmlExporterTest {
// 7. Assertions - Surgical SVG Identification
// Every event should be wrapped in an identifying hyperlink for robust JS selection
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_html.html"), html);
click.kamil.springstatemachineexporter.exporter.ExportOptions plantUmlOptions = click.kamil.springstatemachineexporter.exporter.ExportOptions.builder()
.embedIdentifiers(true)
.build();
click.kamil.springstatemachineexporter.exporter.PlantUml pumlExporter = new click.kamil.springstatemachineexporter.exporter.PlantUml();
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_puml.puml"), pumlExporter.export(result, plantUmlOptions));
assertThat(html).contains("__CREATE");
assertThat(html).contains("__ASSIGN");

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,26 @@
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);
// Functional quirks: passing functions and method references
<T extends IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
java.util.function.Function<T, Void> transitionAction);
// 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,87 @@
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 IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
java.util.function.Function<T, Void> transitionAction) {
// Resolve the event from the getter / supplier function
T event = eventSupplier.get();
// Apply the transition via the provided function / method reference
transitionAction.apply(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,60 @@
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";
}
// --- Functional Quirks for Static Analysis ---
// A getter returning a specific ENUM constant
public OrderEvent getCompleteEvent() {
return OrderEvent.COMPLETE;
}
// A specific function that performs the transition
public Void triggerTransitionFunction(click.kamil.domain.IEvent event) {
stateMachineService.sendMessage(event);
return null;
}
@PostMapping("/functional-complete")
public String functionalCompleteOrder() {
// Pass the getter and the transition applier as method references
stateMachineService.executeFunctionalTransition(
this::getCompleteEvent,
this::triggerTransitionFunction
);
return "Functional complete event triggered via method references";
}
}

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