7 Commits

Author SHA1 Message Date
db58c55b84 attempt to adding spring bean registry 2026-06-20 16:08:17 +02:00
f9e6247eb3 fix 6 2026-06-20 13:20:05 +02:00
76ac143370 more fixes 5 2026-06-20 13:11:54 +02:00
9ca8b2fe37 more fixes 4 2026-06-20 12:31:11 +02:00
56cdf96f2d more fixes 3 2026-06-20 12:21:11 +02:00
c37aa92c78 more fixes 2 2026-06-20 07:57:03 +02:00
3ac057618f more fixes 2026-06-20 07:21:19 +02:00
32 changed files with 3291 additions and 117 deletions

View File

@@ -70,7 +70,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
}
}
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())))) {
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent)))) {
// Event matches or is a wildcard
for (State smSourceState : t.getSourceStates()) {
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
@@ -123,29 +123,160 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
result.setMetadata(updatedMetadata);
}
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();
}
String beanId = chain.getTriggerPoint() != null ? chain.getTriggerPoint().getStateMachineId() : null;
if (targetVar != null && !targetVar.isEmpty()) {
targetVar = targetVar.toLowerCase();
// E.g., if target is "myStateMachine", ensure current machine name contains "my"
if (beanId != null && !beanId.isEmpty() && currentMachineName != null) {
// currentMachineName format: FQN (Entry Point) or FQN#beanMethodName
if (currentMachineName.equals(beanId) || currentMachineName.endsWith("#" + beanId)) {
return true; // Exact match!
}
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;
}
String simplifiedBeanId = beanId.substring(beanId.lastIndexOf('.') + 1).toLowerCase();
// If bean was explicitly identified but doesn't match the current machine, try to reject it
if (!simplifiedMachineName.contains(simplifiedBeanId) && !simplifiedBeanId.contains(simplifiedMachineName)) {
String beanPrefix = getFirstCamelCaseWord(beanId.substring(beanId.lastIndexOf('.') + 1));
String machinePrefix = getFirstCamelCaseWord(currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1));
if (beanPrefix != null && machinePrefix != null && !beanPrefix.equalsIgnoreCase(machinePrefix)) {
// e.g. PaymentStateMachineFactory vs OrderStateMachineConfig
return false;
}
}
}
// Generic approach combining Prefix, Package, and String-distance heuristics
// without relying on a hardcoded list of magic keywords.
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
String smPackage = getPackageName(currentMachineName);
String smSimple = getSimpleClassName(currentMachineName);
String smPrefix = getFirstCamelCaseWord(smSimple);
boolean hasPositiveMatch = false;
boolean hasStrongMismatch = false;
for (String method : chain.getMethodChain()) {
String chainClass = getClassNameOnly(method);
String chainPackage = getPackageName(chainClass);
String chainSimple = getSimpleClassName(chainClass);
String chainPrefix = getFirstCamelCaseWord(chainSimple);
// 1. Explicit Prefix Match (e.g. ElectronicsOrderService & ElectronicsStateMachine)
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
hasPositiveMatch = true;
}
// 2. Explicit Sub-Package Match (e.g. com.company.electronics.service & com.company.electronics.sm)
// We consider it a match if they share a package segment beyond the first 2 (which are usually com.company)
String deepShared = getDeepSharedPackage(smPackage, chainPackage);
if (deepShared != null) {
hasPositiveMatch = true;
}
// Determine if there is a strong mismatch.
// A strong mismatch happens if the chain class has a VERY distinct prefix/package
// that contradicts the SM's distinct prefix/package.
// Without magic keywords, we guess if a prefix is distinct if it's long enough and doesn't match.
if (smPrefix != null && chainPrefix != null && !smPrefix.equals(chainPrefix)) {
// They have different first CamelCase words.
// This could be Electronics vs ComputerStore, or it could be Common vs ComputerStore.
// If they diverge at a deep package level, it's a strong mismatch.
if (isDeepPackageDivergence(smPackage, chainPackage)) {
hasStrongMismatch = true;
}
}
}
// If we found a positive structural match, route it!
if (hasPositiveMatch) {
return true;
}
// If there's no positive match, but we detected a strong structural mismatch, reject it!
if (hasStrongMismatch) {
return false;
}
// If neither, we allow it (fallback for common/shared endpoints without distinct domains)
}
return true;
}
private String simplify(String name) {
private String getPackageName(String fqn) {
if (fqn == null || !fqn.contains(".")) return "";
return fqn.substring(0, fqn.lastIndexOf('.'));
}
private String getSimpleClassName(String fqn) {
if (fqn == null) return "";
if (!fqn.contains(".")) return fqn;
return fqn.substring(fqn.lastIndexOf('.') + 1);
}
private String getClassNameOnly(String methodFqn) {
if (methodFqn == null) return "";
String clean = methodFqn;
if (clean.contains("(")) {
clean = clean.substring(0, clean.indexOf('('));
}
if (clean.contains(".")) {
clean = clean.substring(0, clean.lastIndexOf('.'));
}
return clean;
}
private String getFirstCamelCaseWord(String simpleName) {
if (simpleName == null || simpleName.isEmpty()) return null;
// e.g. "ElectronicsOrderService" -> "Electronics"
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
if (words.length > 0) {
return words[0];
}
return null;
}
private String getDeepSharedPackage(String pkg1, String pkg2) {
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null;
String[] p1 = pkg1.split("\\.");
String[] p2 = pkg2.split("\\.");
// Find the deepest common segment index
int matchIdx = -1;
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
if (p1[i].equals(p2[i])) {
matchIdx = i;
} else {
break;
}
}
// If they share at least 3 segments (e.g. com.company.electronics), it's a deep match
if (matchIdx >= 2) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i <= matchIdx; i++) {
sb.append(p1[i]).append(".");
}
return sb.toString();
}
return null;
}
private boolean isDeepPackageDivergence(String pkg1, String pkg2) {
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return false;
String[] p1 = pkg1.split("\\.");
String[] p2 = pkg2.split("\\.");
// If they share exactly the root (com.company) but diverge at the 3rd segment (e.g. electronics vs computerstore)
// Then it's a divergence.
if (p1.length >= 3 && p2.length >= 3) {
return p1[0].equals(p2[0]) && p1[1].equals(p2[1]) && !p1[2].equals(p2[2]);
}
return false;
}
private String simplify(String name) {
if (name == null) return null;
// Strip common suffixes
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
@@ -157,4 +288,31 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
return simplified;
}
}
private boolean isGuardedContains(String smEvent, String triggerEvent) {
// Only fire if it looks like a simple identifier (no spaces, no parens)
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
return false;
}
String shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
shorter = shorter.toLowerCase();
longer = longer.toLowerCase();
if (longer.contains(shorter)) {
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
if (shorter.length() < 4) {
return longer.matches(".*(^|_|-)" + shorter + "(_|-|$).*");
}
// For longer strings, a straight contains is usually safe enough
return true;
}
return false;
}
}

View File

@@ -21,4 +21,6 @@ public class TriggerPoint {
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
@Builder.Default
private final Map<String, String> payloadTypes = new java.util.HashMap<>();
}

View File

@@ -55,7 +55,26 @@ public class ConstantResolver {
return resolveManual(sn, context, visited);
}
if (expr instanceof MethodInvocation mi) {
if ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) {
String val = resolveInternal(mi.getExpression(), context, visited);
if (val != null) {
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
if (val.contains(".")) {
return val.substring(val.lastIndexOf('.') + 1);
}
return val;
}
// Fallback for unresolved AST nodes
if (mi.getExpression() instanceof QualifiedName qn) {
return qn.getName().getIdentifier();
} else if (mi.getExpression() instanceof SimpleName sn) {
return sn.getIdentifier();
}
}
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
ITypeBinding returnType = mb.getReturnType();
@@ -161,12 +180,44 @@ public class ConstantResolver {
}
}
}
// Fallback: Check constructors for assignments
for (MethodDeclaration md : td.getMethods()) {
if (md.isConstructor() && md.getBody() != null) {
String val = findAssignmentInBlock(md.getBody(), fieldName, context, visited);
if (val != null) return val;
}
}
} finally {
visited.remove(fieldId);
}
return null;
}
private String findAssignmentInBlock(Block block, String fieldName, CodebaseContext context, Set<String> visited) {
if (block == null) return null;
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof ExpressionStatement exprStmt) {
if (exprStmt.getExpression() instanceof Assignment assignment) {
Expression left = assignment.getLeftHandSide();
boolean match = false;
if (left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) {
match = true;
} else if (left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) {
if (fa.getExpression() instanceof ThisExpression) {
match = true;
}
}
if (match) {
return resolveInternal(assignment.getRightHandSide(), context, visited);
}
}
}
}
return null;
}
private String findValueFromAnnotation(FieldDeclaration fd) {
for (Object mod : fd.modifiers()) {
if (mod instanceof Annotation ann) {

View File

@@ -13,6 +13,7 @@ import java.util.*;
@Slf4j
public class CallGraphBuilder {
private final CodebaseContext context;
private final click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver springDependencyResolver;
private final ConstantResolver constantResolver;
private String currentMethodFqn;
private Map<String, List<CallEdge>> graph;
@@ -24,7 +25,12 @@ public class CallGraphBuilder {
}
public CallGraphBuilder(CodebaseContext context) {
this(context, new click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver(context, new click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry(context)));
}
public CallGraphBuilder(CodebaseContext context, click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver springDependencyResolver) {
this.context = context;
this.springDependencyResolver = springDependencyResolver;
this.constantResolver = new ConstantResolver();
}
@@ -159,27 +165,61 @@ public class CallGraphBuilder {
}
String methodName = resolvedValue.substring(lastDot + 1, openParen);
String declaredType = null;
String sourceMethod = null;
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);
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
sourceMethod = methodFqn;
break;
}
}
} else {
String baseExpr = resolvedValue.substring(0, lastDot);
if (baseExpr.contains("new ")) {
int newIdx = baseExpr.indexOf("new ");
int openParenBase = baseExpr.indexOf('(', newIdx);
if (openParenBase > newIdx) {
declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim();
if (declaredType.contains("<")) {
declaredType = declaredType.substring(0, declaredType.indexOf('<'));
}
sourceMethod = "inline-instantiation";
}
}
}
if (declaredType != null) {
System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType);
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
List<String> typesToInspect = new ArrayList<>();
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
if ("inline-instantiation".equals(sourceMethod)) {
typesToInspect.add(declaredType);
} else {
typesToInspect.add(declaredType);
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> resolvedBeans = springDependencyResolver.resolve(declaredType, null, varName);
if (resolvedBeans.isEmpty()) {
typesToInspect.addAll(context.getImplementations(declaredType));
} else {
for (click.kamil.springstatemachineexporter.analysis.spring.SpringBean bean : resolvedBeans) {
if (!bean.getTypeFqn().equals(declaredType)) {
typesToInspect.add(bean.getTypeFqn());
}
}
}
}
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('.'));
String firstPathMethod = path.get(0);
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) entryTd.getRoot();
@@ -193,13 +233,54 @@ public class CallGraphBuilder {
}
}
}
break;
}
}
}
}
}
// LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
// for string literals or enum-like constants.
boolean hasValidConstant = false;
for (String ev : polymorphicEvents) {
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
if (val.equals(val.toUpperCase()) && val.length() > 2) {
hasValidConstant = true;
break;
}
}
if (!hasValidConstant && resolvedValue.contains("(")) {
java.util.List<String> scraped = new java.util.ArrayList<>();
// Extract "STRING_LITERALS"
java.util.regex.Matcher m1 = java.util.regex.Pattern.compile("\"([^\"]+)\"").matcher(resolvedValue);
while (m1.find()) {
scraped.add(m1.group(1));
}
// Extract ENUM_LIKE_CONSTANTS
java.util.regex.Matcher m2 = java.util.regex.Pattern.compile("\\b([A-Z_]{3,})\\b").matcher(resolvedValue);
while (m2.find()) {
if (!scraped.contains(m2.group(1))) {
scraped.add(m2.group(1));
}
}
if (!scraped.isEmpty()) {
polymorphicEvents.clear();
polymorphicEvents.addAll(scraped);
}
}
// Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones
if (polymorphicEvents.size() > 1) {
polymorphicEvents.removeIf(e -> {
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
return !val.equals(val.toUpperCase()) || val.length() <= 2;
});
}
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(resolvedValue)
@@ -274,7 +355,11 @@ public class CallGraphBuilder {
if (!visited.add(fqn)) return Collections.emptyList();
List<String> constants = new ArrayList<>();
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
if (tempTd == null) {
tempTd = context.getTypeDeclaration(className);
}
final TypeDeclaration td = tempTd;
if (td == null) {
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
}
@@ -322,7 +407,12 @@ public class CallGraphBuilder {
} else if (retExpr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (retExpr instanceof SimpleName sn) {
constants.add(sn.toString());
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
if (!consts.isEmpty()) {
constants.addAll(consts);
} else {
constants.add(sn.toString());
}
}
}
}
@@ -465,9 +555,23 @@ public class CallGraphBuilder {
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));
String receiverName = null;
if (emr.getExpression() instanceof SimpleName sn) {
receiverName = sn.getIdentifier();
}
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> resolvedBeans = springDependencyResolver.resolve(fallbackTypeFqn, null, receiverName);
if (resolvedBeans.isEmpty()) {
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
}
} else {
for (click.kamil.springstatemachineexporter.analysis.spring.SpringBean bean : resolvedBeans) {
if (!bean.getTypeFqn().equals(fallbackTypeFqn)) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(bean.getTypeFqn() + "." + emr.getName().getIdentifier(), args));
}
}
}
}
}
@@ -574,21 +678,91 @@ public class CallGraphBuilder {
if (baseCalled == null) return Collections.emptyList();
List<String> allResolved = new ArrayList<>();
allResolved.add(baseCalled);
if (baseCalled.contains(".")) {
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
List<String> impls = context.getImplementations(className);
for (String impl : impls) {
allResolved.add(impl + "." + methodName);
String receiverName = null;
if (node.getExpression() instanceof SimpleName sn) {
receiverName = sn.getIdentifier();
} else if (node.getExpression() instanceof FieldAccess fa) {
receiverName = fa.getName().getIdentifier();
}
String qualifier = extractQualifierFromVariable(node, receiverName);
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> resolvedBeans = springDependencyResolver.resolve(className, qualifier, receiverName);
if (resolvedBeans.isEmpty()) {
allResolved.add(baseCalled);
List<String> impls = context.getImplementations(className);
for (String impl : impls) {
allResolved.add(impl + "." + methodName);
}
} else {
for (click.kamil.springstatemachineexporter.analysis.spring.SpringBean bean : resolvedBeans) {
allResolved.add(bean.getTypeFqn() + "." + methodName);
}
}
} else {
allResolved.add(baseCalled);
}
return allResolved;
}
private String extractQualifierFromVariable(ASTNode node, String varName) {
if (varName == null) return null;
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
for (Object paramObj : md.parameters()) {
if (paramObj instanceof SingleVariableDeclaration svd) {
if (svd.getName().getIdentifier().equals(varName)) {
return extractQualifierFromModifiers(svd.modifiers());
}
}
}
}
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment vdf) {
if (vdf.getName().getIdentifier().equals(varName)) {
return extractQualifierFromModifiers(fd.modifiers());
}
}
}
}
}
return null;
}
private String extractQualifierFromModifiers(List<?> modifiers) {
for (Object modObj : modifiers) {
if (modObj instanceof NormalAnnotation na) {
if ("Qualifier".equals(na.getTypeName().getFullyQualifiedName()) || "org.springframework.beans.factory.annotation.Qualifier".equals(na.getTypeName().getFullyQualifiedName())) {
for (Object valObj : na.values()) {
if (valObj instanceof MemberValuePair mvp) {
if ("value".equals(mvp.getName().getIdentifier()) && mvp.getValue() instanceof StringLiteral sl) {
return sl.getLiteralValue();
}
}
}
}
} else if (modObj instanceof SingleMemberAnnotation sma) {
if ("Qualifier".equals(sma.getTypeName().getFullyQualifiedName()) || "org.springframework.beans.factory.annotation.Qualifier".equals(sma.getTypeName().getFullyQualifiedName())) {
if (sma.getValue() instanceof StringLiteral sl) {
return sl.getLiteralValue();
}
}
}
}
return null;
}
private String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier();
@@ -743,10 +917,25 @@ public class CallGraphBuilder {
}
private boolean isHeuristicMatch(String neighbor, String target) {
if (neighbor.equals(target)) return true;
if (target.endsWith("." + neighbor)) return true;
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
return simpleNeighbor.equals(simpleTarget);
int nIdx = neighbor.lastIndexOf('.');
int tIdx = target.lastIndexOf('.');
if (nIdx > 0 && tIdx > 0) {
String nClass = neighbor.substring(0, nIdx);
String nMethod = neighbor.substring(nIdx + 1);
String tClass = target.substring(0, tIdx);
String tMethod = target.substring(tIdx + 1);
if (nMethod.equals(tMethod)) {
List<String> impls = context.getImplementations(nClass);
if (impls.contains(tClass)) {
return true;
}
}
}
return false;
}
private TypeDeclaration findEnclosingType(ASTNode node) {
@@ -785,4 +974,113 @@ public class CallGraphBuilder {
}
return expr;
}
}
private List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
List<String> results = new ArrayList<>();
// 1. Check Field Declarations
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
Expression right = frag.getInitializer();
if (right instanceof MethodInvocation mi) {
String calledName = mi.getName().getIdentifier();
String targetClass = context.getFqn(td);
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
} else {
String val = constantResolver.resolve(right, context);
if (val != null) results.add(val);
}
}
}
}
org.eclipse.jdt.core.dom.ASTVisitor assignmentVisitor = new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(Assignment node) {
Expression left = node.getLeftHandSide();
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
Expression right = node.getRightHandSide();
if (right instanceof MethodInvocation mi) {
String calledName = mi.getName().getIdentifier();
String targetClass = context.getFqn(td);
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
} else if (mi.getExpression() instanceof SimpleName) {
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
if (targetTd != null) {
targetClass = context.getFqn(targetTd);
}
}
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
} else {
String val = constantResolver.resolve(right, context);
if (val != null) results.add(val);
}
}
return super.visit(node);
}
@Override
public boolean visit(ConstructorInvocation node) {
for (Object argObj : node.arguments()) {
Expression arg = (Expression) argObj;
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
for (Object argObj : node.arguments()) {
Expression arg = (Expression) argObj;
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
}
}
return super.visit(node);
}
};
// 2. Check Constructors
for (MethodDeclaration md : td.getMethods()) {
if (md.isConstructor() && md.getBody() != null) {
md.getBody().accept(assignmentVisitor);
}
}
// 3. Check Initializers
for (Object bodyDecl : td.bodyDeclarations()) {
if (bodyDecl instanceof org.eclipse.jdt.core.dom.Initializer init && init.getBody() != null) {
init.getBody().accept(assignmentVisitor);
}
}
return results;
}
}

View File

@@ -18,6 +18,11 @@ public class GenericEventDetector {
private final CodebaseContext context;
private final ConstantResolver constantResolver;
private final List<LibraryHint> hints;
private final click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver springDependencyResolver;
public GenericEventDetector(CodebaseContext context, ConstantResolver constantResolver, List<LibraryHint> hints) {
this(context, constantResolver, hints, new click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver(context, new click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry(context)));
}
public List<TriggerPoint> detect(CompilationUnit cu) {
List<TriggerPoint> triggers = new ArrayList<>();
@@ -142,6 +147,28 @@ public class GenericEventDetector {
String sourceState = extractSourceState(node);
String stateMachineId = null;
if (node.getExpression() instanceof SimpleName sn) {
String varName = sn.getIdentifier();
String className = resolveReceiverTypeFallback(sn, cu);
if (className != null && springDependencyResolver != null) {
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> beans = springDependencyResolver.resolve(className, null, varName);
if (beans.size() == 1) {
if (!beans.get(0).getBeanNames().isEmpty()) {
stateMachineId = beans.get(0).getBeanNames().iterator().next();
} else {
stateMachineId = beans.get(0).getTypeFqn();
}
}
}
}
java.util.Map<String, String> payloadTypes = java.util.Collections.emptyMap();
if (forcedEvent == null && !node.arguments().isEmpty()) {
Expression eventExpr = (Expression) node.arguments().get(0);
payloadTypes = extractPayloadsFromMessageBuilder(eventExpr, cu);
}
List<TriggerPoint> results = new ArrayList<>();
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
String[] parts = eventValue.substring(9).split(",");
@@ -150,10 +177,12 @@ public class GenericEventDetector {
results.add(TriggerPoint.builder()
.event(part.trim())
.sourceState(sourceState)
.stateMachineId(stateMachineId)
.className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition()))
.payloadTypes(payloadTypes)
.build());
}
}
@@ -161,10 +190,12 @@ public class GenericEventDetector {
results.add(TriggerPoint.builder()
.event(eventValue)
.sourceState(sourceState)
.stateMachineId(stateMachineId)
.className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition()))
.payloadTypes(payloadTypes)
.build());
}
@@ -226,6 +257,78 @@ public class GenericEventDetector {
return expr.toString();
}
private java.util.Map<String, String> extractPayloadsFromMessageBuilder(Expression expr, CompilationUnit cu) {
if (expr instanceof CastExpression ce) {
return extractPayloadsFromMessageBuilder(ce.getExpression(), cu);
}
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 extractPayloadsFromMessageBuilder(initializer[0], cu);
}
}
}
if (!(expr instanceof MethodInvocation mi)) return java.util.Collections.emptyMap();
java.util.Map<String, String> payloads = new java.util.HashMap<>();
MethodInvocation current = mi;
while (current != null) {
String name = current.getName().getIdentifier();
if ("setHeader".equals(name) && current.arguments().size() == 2) {
Expression keyExpr = (Expression) current.arguments().get(0);
Expression valueExpr = (Expression) current.arguments().get(1);
String key = constantResolver.resolve(keyExpr, context);
if (key == null) {
if (keyExpr instanceof StringLiteral sl) key = sl.getLiteralValue();
else key = keyExpr.toString();
}
String type = null;
if (valueExpr instanceof SimpleName vSn) {
type = resolveReceiverTypeFallback(vSn, cu);
}
if (type == null) type = "Object";
// Keep simple type name if possible
if (type.contains(".")) {
type = type.substring(type.lastIndexOf('.') + 1);
}
payloads.put(key, type);
}
Expression receiver = current.getExpression();
if (receiver instanceof MethodInvocation nextMi) {
current = nextMi;
} else {
current = null;
}
}
return payloads;
}
private String extractEventFromMessageBuilder(Expression expr) {
if (expr instanceof CastExpression ce) {
return extractEventFromMessageBuilder(ce.getExpression());
@@ -316,6 +419,77 @@ public class GenericEventDetector {
return null;
}
private String resolveReceiverTypeFallback(SimpleName receiverNameNode, CompilationUnit cu) {
String varName = receiverNameNode.getIdentifier();
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
if (enclosingMethod != null) {
for (Object paramObj : enclosingMethod.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
if (svd.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(svd.getType(), cu);
}
}
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], cu);
}
}
}
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(), cu);
}
}
}
}
return null;
}
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu);
} else {
simpleName = type.toString();
}
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
return simpleName;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {

View File

@@ -8,6 +8,9 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.CompilationUnit;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -25,14 +28,22 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
private final LifecycleDetector lifecycleDetector;
private final InterceptorDetector interceptorDetector;
private final SpringComponentDetector componentDetector;
private final SpringBeanRegistry springBeanRegistry;
private final SpringDependencyResolver springDependencyResolver;
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
this.context = context;
this.rootDir = rootDir;
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
// Initialize Spring Bean Context early
this.springBeanRegistry = new click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry(context);
this.springBeanRegistry.discoverBeans();
this.springDependencyResolver = new click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver(context, springBeanRegistry);
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints(), springDependencyResolver);
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
this.messagingDetector = new MessagingDetector(context);
this.callGraphBuilder = new CallGraphBuilder(context);
this.callGraphBuilder = new CallGraphBuilder(context, springDependencyResolver);
this.lifecycleDetector = new LifecycleDetector(context);
this.interceptorDetector = new InterceptorDetector(context);
this.componentDetector = new SpringComponentDetector(context);

View File

@@ -0,0 +1,43 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import java.util.HashSet;
import java.util.Set;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class SpringBean {
/**
* The fully qualified name of the bean's actual type.
*/
private String typeFqn;
/**
* The names under which this bean is registered (could be multiple if aliases exist).
*/
@Builder.Default
private Set<String> beanNames = new HashSet<>();
/**
* The qualifiers associated with this bean.
*/
@Builder.Default
private Set<String> qualifiers = new HashSet<>();
/**
* True if the bean is annotated with @Primary.
*/
private boolean isPrimary;
/**
* The FQN of the class where this bean is declared (e.g. the @Configuration class).
*/
private String declaringClassFqn;
/**
* Factory method name if the bean was created via @Bean. Null if it's a component.
*/
private String factoryMethodName;
}

View File

@@ -0,0 +1,256 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class SpringBeanRegistry {
private final CodebaseContext context;
private final List<SpringBean> beans = new ArrayList<>();
private static final Set<String> STEREOTYPES = Set.of(
"Component", "Service", "Repository", "Controller", "RestController", "Configuration"
);
public SpringBeanRegistry(CodebaseContext context) {
this.context = context;
}
public void discoverBeans() {
for (TypeDeclaration td : context.getTypeDeclarations()) {
if (td.isInterface()) continue; // Interfaces themselves aren't beans unless they have @Bean methods, but let's stick to classes for now.
// 1. Check if the class itself is a bean (Stereotype annotations)
if (isStereotypeAnnotated(td)) {
registerClassBean(td);
}
// 2. Check for @Bean methods inside the class
for (MethodDeclaration md : td.getMethods()) {
if (hasBeanAnnotation(md)) {
registerMethodBean(td, md);
}
}
}
}
private void registerClassBean(TypeDeclaration td) {
String fqn = context.getFqn(td);
if (fqn == null) return;
Set<String> beanNames = new HashSet<>();
Set<String> qualifiers = new HashSet<>();
boolean isPrimary = false;
String defaultName = getDefaultBeanName(td.getName().getIdentifier());
// Extract names and qualifiers from annotations
for (Object modObj : td.modifiers()) {
if (modObj instanceof Annotation ann) {
String annName = ann.getTypeName().getFullyQualifiedName();
String simpleName = getSimpleName(annName);
if (STEREOTYPES.contains(simpleName)) {
String value = extractValue(ann);
if (value != null && !value.isEmpty()) {
beanNames.add(value);
}
} else if ("Qualifier".equals(simpleName)) {
String value = extractValue(ann);
if (value != null && !value.isEmpty()) {
qualifiers.add(value);
}
} else if ("Primary".equals(simpleName)) {
isPrimary = true;
}
}
}
if (beanNames.isEmpty()) {
beanNames.add(defaultName);
}
beans.add(SpringBean.builder()
.typeFqn(fqn)
.beanNames(beanNames)
.qualifiers(qualifiers)
.isPrimary(isPrimary)
.declaringClassFqn(fqn)
.build());
}
private void registerMethodBean(TypeDeclaration td, MethodDeclaration md) {
Type returnType = md.getReturnType2();
if (returnType == null) return;
String declaredTypeFqn = resolveTypeToFqn(returnType, td);
if (declaredTypeFqn == null) return;
Set<String> beanNames = new HashSet<>();
Set<String> qualifiers = new HashSet<>();
boolean isPrimary = false;
String defaultName = md.getName().getIdentifier();
for (Object modObj : md.modifiers()) {
if (modObj instanceof Annotation ann) {
String annName = ann.getTypeName().getFullyQualifiedName();
String simpleName = getSimpleName(annName);
if ("Bean".equals(simpleName)) {
List<String> values = extractArrayValues(ann);
if (!values.isEmpty()) {
beanNames.addAll(values);
}
} else if ("Qualifier".equals(simpleName)) {
String value = extractValue(ann);
if (value != null && !value.isEmpty()) {
qualifiers.add(value);
}
} else if ("Primary".equals(simpleName)) {
isPrimary = true;
}
}
}
if (beanNames.isEmpty()) {
beanNames.add(defaultName);
}
beans.add(SpringBean.builder()
.typeFqn(declaredTypeFqn)
.beanNames(beanNames)
.qualifiers(qualifiers)
.isPrimary(isPrimary)
.declaringClassFqn(context.getFqn(td))
.factoryMethodName(md.getName().getIdentifier())
.build());
}
private boolean isStereotypeAnnotated(TypeDeclaration td) {
return checkStereotypeAnnotated(td, new HashSet<>());
}
private boolean checkStereotypeAnnotated(TypeDeclaration td, Set<String> visited) {
String fqn = context.getFqn(td);
if (fqn != null) {
if (!visited.add(fqn)) return false; // Cycle detection
}
for (Object modObj : td.modifiers()) {
if (modObj instanceof Annotation ann) {
String simpleName = getSimpleName(ann.getTypeName().getFullyQualifiedName());
if (STEREOTYPES.contains(simpleName)) {
return true;
}
}
}
// Could check superclasses/interfaces if we want to support inherited annotations,
// but typically Spring Boot relies on annotations on the concrete class for components.
return false;
}
private boolean hasBeanAnnotation(MethodDeclaration md) {
for (Object modObj : md.modifiers()) {
if (modObj instanceof Annotation ann) {
String simpleName = getSimpleName(ann.getTypeName().getFullyQualifiedName());
if ("Bean".equals(simpleName)) {
return true;
}
}
}
return false;
}
private String getSimpleName(String fqn) {
return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
}
private String getDefaultBeanName(String className) {
if (className == null || className.isEmpty()) return className;
return Character.toLowerCase(className.charAt(0)) + className.substring(1);
}
private String extractValue(Annotation ann) {
if (ann instanceof SingleMemberAnnotation sma) {
return stripQuotes(sma.getValue().toString());
} else if (ann instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if ("value".equals(pair.getName().getIdentifier()) || "name".equals(pair.getName().getIdentifier())) {
return stripQuotes(pair.getValue().toString());
}
}
}
return null;
}
private List<String> extractArrayValues(Annotation ann) {
List<String> results = new ArrayList<>();
if (ann instanceof SingleMemberAnnotation sma) {
Expression expr = sma.getValue();
if (expr instanceof ArrayInitializer arr) {
for (Object item : arr.expressions()) {
results.add(stripQuotes(item.toString()));
}
} else {
results.add(stripQuotes(expr.toString()));
}
} else if (ann instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if ("value".equals(pair.getName().getIdentifier()) || "name".equals(pair.getName().getIdentifier())) {
Expression expr = pair.getValue();
if (expr instanceof ArrayInitializer arr) {
for (Object item : arr.expressions()) {
results.add(stripQuotes(item.toString()));
}
} else {
results.add(stripQuotes(expr.toString()));
}
}
}
}
return results;
}
private String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
private String resolveTypeToFqn(Type type, TypeDeclaration contextTd) {
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextTd);
} else {
simpleName = type.toString();
}
CompilationUnit cu = (CompilationUnit) contextTd.getRoot();
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
// Fallback to import matching
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
return simpleName;
}
public List<SpringBean> getBeans() {
return Collections.unmodifiableList(beans);
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class SpringDependencyResolver {
private final CodebaseContext context;
private final SpringBeanRegistry registry;
public SpringDependencyResolver(CodebaseContext context, SpringBeanRegistry registry) {
this.context = context;
this.registry = registry;
}
/**
* Resolves the exact SpringBean that would be injected for a given injection point.
*
* @param requiredTypeFqn The FQN of the type being injected.
* @param qualifier The value of the @Qualifier annotation (if any).
* @param injectionName The name of the field or parameter being injected (used as fallback).
* @return A list of resolved beans. Usually 1. If 0, unresolved. If > 1, ambiguous or a Collection injection.
*/
public List<SpringBean> resolve(String requiredTypeFqn, String qualifier, String injectionName) {
if (requiredTypeFqn == null) return new ArrayList<>();
// 1. Gather all acceptable types (the required type itself + all implementations)
Set<String> acceptableTypes = new HashSet<>();
acceptableTypes.add(requiredTypeFqn);
acceptableTypes.addAll(context.getImplementations(requiredTypeFqn));
// 2. Filter by Type
List<SpringBean> candidates = registry.getBeans().stream()
.filter(bean -> acceptableTypes.contains(bean.getTypeFqn()))
.collect(Collectors.toList());
if (candidates.isEmpty() || candidates.size() == 1) {
return candidates;
}
// 3. Filter by Qualifier (if provided)
if (qualifier != null && !qualifier.isEmpty()) {
List<SpringBean> qualifiedCandidates = candidates.stream()
.filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier))
.collect(Collectors.toList());
if (qualifiedCandidates.size() == 1) {
return qualifiedCandidates;
} else if (!qualifiedCandidates.isEmpty()) {
candidates = qualifiedCandidates;
}
}
// 4. Filter by @Primary
List<SpringBean> primaryCandidates = candidates.stream()
.filter(SpringBean::isPrimary)
.collect(Collectors.toList());
if (primaryCandidates.size() == 1) {
return primaryCandidates;
}
// 5. Fallback to Injection Name (field name or parameter name)
if (injectionName != null && !injectionName.isEmpty()) {
List<SpringBean> namedCandidates = candidates.stream()
.filter(bean -> bean.getBeanNames().contains(injectionName))
.collect(Collectors.toList());
if (namedCandidates.size() == 1) {
return namedCandidates;
}
}
// Return whatever candidates remain (could be ambiguous)
return candidates;
}
}

View File

@@ -94,6 +94,10 @@ public class CodebaseContext {
this.propertyResolver.setActiveProfiles(profiles);
}
public List<String> getActiveProfiles() {
return Collections.unmodifiableList(activeProfiles);
}
public String resolveString(String input) {
if (input == null) return null;

View File

@@ -45,6 +45,18 @@ public class ExportService {
new EntryPointEnricher(),
new PropertyEnricher(),
new CallChainEnricher(),
(result, context, intelligence) -> {
// Force property resolution before transition linking
if (result.getMetadata() != null && result.getMetadata().getProperties() != null) {
Map<String, Map<String, String>> allProps = result.getMetadata().getProperties();
Map<String, String> merged = new HashMap<>();
if (allProps.containsKey("default")) merged.putAll(allProps.get("default"));
for (String profile : context.getActiveProfiles()) {
if (allProps.containsKey(profile)) merged.putAll(allProps.get(profile));
}
result.applyResolution(merged);
}
},
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
)));
}

View File

@@ -192,4 +192,190 @@ class TransitionLinkerEnricherTest {
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
}
@Test
void shouldNotFalsePositiveOnSubstringContains() {
// This test proves that the heuristic is entirely removed and false positives are eliminated
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"));
// Trigger point sends "PAYMENT" which contains "PAY". Under the old heuristics, this would falsely match!
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("PAYMENT").build())
.build();
AnalysisResult result = AnalysisResult.builder()
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
// It should NOT match, because "PAYMENT" != "PAY" strictly.
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
}
@Test
void shouldFilterCrossStateMachineRoutingByVertical() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of(
"com.example.electronics.ElectronicsOrderController.post()",
"com.example.electronics.ElectronicsOrderService.processOrderEvent()"
))
.build();
// Testing against Electronics SM - should match
AnalysisResult resultElectronics = AnalysisResult.builder()
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultElectronics, null, null);
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
// Testing against ComputerStore SM - should NOT match because chain has Electronics service
AnalysisResult resultComputer = AnalysisResult.builder()
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration") // the non-electronics one
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultComputer, null, null);
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
assertThat(updatedChainComp.getMatchedTransitions()).isNullOrEmpty();
}
@Test
void shouldAllowSharedServicesToRouteToMultipleStateMachines() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PROCESSED", "PROCESSED")));
t1.setEvent(Event.of("PROCESS", "PROCESS"));
// Chain contains NO vertical prefix (e.g. CommonOrderService)
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("PROCESS").build())
.methodChain(List.of(
"com.example.CommonOrderController.post()",
"com.example.CommonOrderService.processOrderEvent()"
))
.build();
AnalysisResult resultElectronics = AnalysisResult.builder()
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultElectronics, null, null);
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
// Should match Electronics because it's a shared service (no computerstore service in path)
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
AnalysisResult resultComputer = AnalysisResult.builder()
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultComputer, null, null);
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
// Should ALSO match ComputerStore because it's a shared service
assertThat(updatedChainComp.getMatchedTransitions()).hasSize(1);
}
@Test
void shouldFilterCrossStateMachineRoutingWithMultipleVerticals() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
// Chain 1: Electronics
CallChain chainElec = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of("com.example.electronics.ElectronicsOrderController.post()"))
.build();
// Chain 2: Furniture
CallChain chainFurn = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of("com.example.furniture.FurnitureOrderController.post()"))
.build();
// Chain 3: Groceries
CallChain chainGroc = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of("com.example.groceries.GroceriesOrderController.post()"))
.build();
// SM 1: Electronics
AnalysisResult resElec = AnalysisResult.builder()
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// SM 2: Furniture
AnalysisResult resFurn = AnalysisResult.builder()
.name("com.example.furniture.FurnitureOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// SM 3: Groceries
AnalysisResult resGroc = AnalysisResult.builder()
.name("com.example.groceries.GroceriesStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// SM 4: Computer Store (The catch-all base one in computerstore package)
AnalysisResult resComp = AnalysisResult.builder()
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// Test Electronics SM - Should only keep Electronics Chain
enricher.enrich(resElec, null, null);
assertThat(resElec.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1); // Elec
assertThat(resElec.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
assertThat(resElec.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
// Test Furniture SM - Should only keep Furniture Chain
enricher.enrich(resFurn, null, null);
assertThat(resFurn.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
assertThat(resFurn.getMetadata().getCallChains().get(1).getMatchedTransitions()).hasSize(1); // Furn
assertThat(resFurn.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
// Test Groceries SM - Should only keep Groceries Chain
enricher.enrich(resGroc, null, null);
assertThat(resGroc.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
assertThat(resGroc.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
assertThat(resGroc.getMetadata().getCallChains().get(2).getMatchedTransitions()).hasSize(1); // Groc
// Test ComputerStore SM - Should reject ALL because none belong to computerstore
enricher.enrich(resComp, null, null);
assertThat(resComp.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
assertThat(resComp.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
assertThat(resComp.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
}
}

View File

@@ -1081,4 +1081,820 @@ class CallGraphBuilderTest {
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B");
}
}
@Test
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventForStatus());
}
}
class DomainEvent {
private OrderStatus status;
public String getEventForStatus() {
switch (status) {
case NEW: return "CREATE_EVENT";
case PAID: return "PAY_EVENT";
default: return "CANCEL_EVENT";
}
}
}
class OrderService {
public void process(String event) {}
}
enum OrderStatus { NEW, PAID }
""";
Path tempDir = Files.createTempDirectory("callgraph_switch");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT");
}
@Test
void shouldGracefullyHandleComplexMethodChainsWithoutCrashing() throws IOException {
String source = """
package com.example;
import java.util.Optional;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(Optional.of(domainEvent).get().getEvent());
}
}
class DomainEvent {
public String getEvent() { return "COMPLEX_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_complex_chain");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
}
@Test
void shouldNotCrashOnStaticMethodWrapping() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(String.valueOf(domainEvent.getEvent()));
}
}
class DomainEvent {
public String getEvent() { return "STATIC_WRAP_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_static_wrap");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
}
@Test
void shouldResolveConstantsFromStringConcatenation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getCustomEvent());
}
}
class DomainEvent {
public static final String PREFIX = "ORDER_";
public String getCustomEvent() {
return PREFIX + "CREATED";
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_concat");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_CREATED");
}
@Test
void shouldResolveConstantsFromEnumReturn() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventEnum());
}
}
class DomainEvent {
public String getEventEnum() {
return MyEvents.ORDER_PAID.name();
}
}
class OrderService {
public void process(String event) {}
}
enum MyEvents { ORDER_PAID }
""";
Path tempDir = Files.createTempDirectory("callgraph_enum_return");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_PAID");
}
@Test
void shouldResolveConstantsFromAlternativeGetterName() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.get2Type());
}
}
class DomainEvent {
public String get2Type() {
return "ALTERNATIVE_EVENT";
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_alt_getter");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ALTERNATIVE_EVENT");
}
@Test
void shouldResolveConstantsFromStringToString() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventString());
}
}
class DomainEvent {
public static final String ACTUAL_STRING = "STRING_LITERAL_EVENT";
public String getEventString() {
return ACTUAL_STRING.toString();
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_string_tostring");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("STRING_LITERAL_EVENT");
}
@Test
void shouldResolveConstantsFromInlineInstantiation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent().getEvent());
}
}
class DomainEvent {
public String getEvent() { return "INLINE_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent("PAYLOAD_ID").getEvent());
}
}
class DomainEvent {
private String id;
public DomainEvent(String id) { this.id = id; }
public String getEvent() { return "INLINE_EVENT_WITH_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_args");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_ARGS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithGenerics() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent<String>().getEvent());
}
}
class DomainEvent<T> {
public String getEvent() { return "INLINE_EVENT_WITH_GENERICS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_gen");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_GENERICS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationInheritedMethod() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new SpecificDomainEvent().getEvent());
}
}
abstract class BaseDomainEvent {
public String getEvent() { return "BASE_EVENT_RETURN"; }
}
class SpecificDomainEvent extends BaseDomainEvent {
// no getEvent here, it inherits it!
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_hier");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithComplexArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(Order o, String abc) {
service.process(new DomainEvent(o.getId(), abc).getEvent());
}
}
class Order {
public String getId() { return "123"; }
}
class DomainEvent {
private String id;
private String extra;
public DomainEvent(String id, String extra) { this.id = id; this.extra = extra; }
public String getEvent() { return "INLINE_EVENT_COMPLEX_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_complex_args");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS");
}
@Test
void shouldNotIncludeSubclassesForInlineInstantiation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new BaseEvent().getEvent());
}
}
class BaseEvent {
public String getEvent() { return "BASE_EVENT_RETURN"; }
}
class SubEvent extends BaseEvent {
public String getEvent() { return "SUB_EVENT_RETURN"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_subclass");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
// Only BASE_EVENT_RETURN should be found. SUB_EVENT_RETURN is a subclass and should not be included for direct instantiations
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
}
@Test
void shouldResolveConstantsFromFieldAssignedInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderPickedUpEvent().getType());
}
}
class OrderPickedUpEvent {
private String type;
public OrderPickedUpEvent() {
this.type = "PICKED_UP_IN_CONSTRUCTOR";
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("PICKED_UP_IN_CONSTRUCTOR");
}
@Test
void shouldResolveConstantsFromFieldAssignedInSuperConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderCancelledEvent().getType());
}
}
abstract class BaseEvent {
private String type;
public BaseEvent(String type, String otherVar) {
this.type = type;
}
public String getType() { return type; }
}
class OrderCancelledEvent extends BaseEvent {
public OrderCancelledEvent() {
super("CANCELLED_IN_SUPER", "someVar");
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_super_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_IN_SUPER");
}
@Test
void shouldResolveConstantsFromFieldAssignedToAnotherConstantInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventWithIndirectConstant().getType());
}
}
class EventConstants {
public static final String INDIRECT_EVENT = "INDIRECT_EVENT_VALUE";
}
class EventWithIndirectConstant {
private String type;
public EventWithIndirectConstant() {
this.type = EventConstants.INDIRECT_EVENT;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_indirect_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("INDIRECT_EVENT_VALUE");
}
@Test
void shouldResolveConstantsFromConstructorInvocation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventWithThisCall().getType());
}
}
class EventWithThisCall {
private String type;
public EventWithThisCall() {
this("THIS_CALL_CONSTANT");
}
public EventWithThisCall(String type) {
this.type = type;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_this_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
}
@Test
void shouldResolveConstantsFromDynamicMethodAssignmentInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderCancelledEvent().getType());
}
}
class OrderCancelledEvent {
private String cancelEventType;
public OrderCancelledEvent() {
this.cancelEventType = assertMatchingCancelEventType("some_sender");
}
private String assertMatchingCancelEventType(String sender) {
return "CANCELLED_BY_SENDER";
}
public String getType() { return cancelEventType; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_dynamic_rhs");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_BY_SENDER");
}
@Test
void shouldResolveConstantsFromAbstractSuperclass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new PostAcceptanceOrderModificationsRejectedEvent().getType());
}
}
abstract class AbstractPostAcceptanceOrderModificationsEvent {
private String event;
public AbstractPostAcceptanceOrderModificationsEvent(String event) {
this.event = event;
}
public String getType() { return event; }
}
class PostAcceptanceOrderModificationsRejectedEvent extends AbstractPostAcceptanceOrderModificationsEvent {
public PostAcceptanceOrderModificationsRejectedEvent() {
super("POST_ACCEPTANCE_REJECTED");
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_post_acc");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("POST_ACCEPTANCE_REJECTED");
}
@Test
void shouldResolveConstantsFromFieldDeclarationsAndInitializers() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus1() {
service.process(new EventWithFieldDecl().getType());
}
public void handleStatus2() {
service.process(new EventWithInitBlock().getType());
}
}
class EventWithFieldDecl {
private String event = "FIELD_DECL";
public String getType() { return event; }
}
class EventWithInitBlock {
private String event;
{
event = "INIT_BLOCK";
}
public String getType() { return event; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_init");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(ep1, ep2), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
java.util.List<String> allEvents = new java.util.ArrayList<>();
chains.forEach(c -> allEvents.addAll(c.getTriggerPoint().getPolymorphicEvents()));
org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK");
}
@Test
void shouldFallbackToScraperForComplexInlineInstantiations() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
// The AST tracer will trace EventWithConstructorArg.getType()
// It will find `this.type = typeArg`, which it cannot resolve because it's a parameter.
// The fallback scraper should catch "COMPLEX_INLINE_CONST".
service.process(new EventWithConstructorArg("COMPLEX_INLINE_CONST", 123).getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_scraper");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
}
}

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "PLACE_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
@@ -19,7 +20,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "CANCEL_ORDER",
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
@@ -29,7 +31,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "PAY_ORDER",
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
@@ -39,7 +42,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "SHIP_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
@@ -49,7 +53,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "RETURN_ORDER",
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
@@ -59,7 +64,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -192,7 +198,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -227,7 +234,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -257,7 +265,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -288,7 +297,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -323,7 +333,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -358,7 +369,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
@@ -19,17 +20,52 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "FALLBACK_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "PROFILED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"stateMachineId" : "externalNotificationService",
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -39,7 +75,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "CANCEL_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -49,7 +86,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -59,7 +97,65 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -69,7 +165,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -144,6 +241,80 @@
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
} ],
"callChains" : [ {
"entryPoint" : {
@@ -165,17 +336,14 @@
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"stateMachineId" : "externalNotificationService",
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
} ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
@@ -199,7 +367,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -230,7 +399,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -261,7 +431,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -296,7 +467,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : "orderId",
"matchedTransitions" : null
@@ -327,7 +499,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -357,7 +530,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -365,6 +539,216 @@
"targetState" : "START",
"event" : "AUDIT_EVENT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
"triggerPoint" : {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
} ],
"properties" : {
"default" : {

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
@@ -19,17 +20,52 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "FALLBACK_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "PROFILED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"stateMachineId" : "externalNotificationService",
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -39,7 +75,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "CANCEL_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -49,7 +86,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -59,7 +97,65 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -69,7 +165,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -144,6 +241,80 @@
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
} ],
"callChains" : [ {
"entryPoint" : {
@@ -165,17 +336,14 @@
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"stateMachineId" : "externalNotificationService",
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
} ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
@@ -199,7 +367,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -230,7 +399,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -261,7 +431,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 34,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -296,7 +467,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 29,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : "orderId",
"matchedTransitions" : null
@@ -327,7 +499,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -357,7 +530,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -365,6 +539,216 @@
"targetState" : "START",
"event" : "AUDIT_EVENT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
"triggerPoint" : {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
} ],
"properties" : {
"default" : {

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 15,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -57,7 +58,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 15,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 40,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "ORDER_EVENT",
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
@@ -19,7 +20,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 52,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -106,7 +108,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 40,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -69,7 +70,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "eventProvider",
"className" : "click.kamil.service.StateMachineServiceImpl",
@@ -19,7 +20,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 50,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
}, {
"event" : "customMessage",
"className" : "click.kamil.service.StateMachineServiceImpl",
@@ -29,7 +31,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 78,
"polymorphicEvents" : null
"polymorphicEvents" : null,
"payloadTypes" : { }
} ],
"entryPoints" : [ {
"type" : "REST",
@@ -144,7 +147,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ ],
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -175,7 +179,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ ],
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -206,7 +211,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ ],
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -241,7 +247,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ ],
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -276,7 +283,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 78,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ ],
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -311,7 +319,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ ],
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -346,7 +355,8 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 50,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ ],
"payloadTypes" : { }
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -0,0 +1,58 @@
package click.kamil.examples.statemachine.extended.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
@EnableStateMachine(name = "paymentStateMachine")
public class PaymentStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("NEW")
.state("AUTHORIZED")
.state("CAPTURED")
.state("DECLINED")
.state("QUIRK1")
.state("QUIRK2")
.state("QUIRK3")
.state("QUIRK4");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("NEW").target("AUTHORIZED")
.event("AUTHORIZE")
.and()
.withExternal()
.source("AUTHORIZED").target("CAPTURED")
.event("CAPTURE")
.and()
.withExternal()
.source("NEW").target("DECLINED")
.event("DECLINE")
.and()
.withExternal()
.source("NEW").target("QUIRK1")
.event("PRIMARY_EVENT")
.and()
.withExternal()
.source("NEW").target("QUIRK2")
.event("NAMED_EVENT")
.and()
.withExternal()
.source("NEW").target("QUIRK3")
.event("QUALIFIER_EVENT")
.and()
.withExternal()
.source("NEW").target("QUIRK4")
.event("FALLBACK_EVENT");
}
}

View File

@@ -0,0 +1,19 @@
package click.kamil.examples.statemachine.extended.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service
public class FallbackQuirkService implements QuirkService {
@Autowired
@Qualifier("paymentStateMachine")
private StateMachine<String, String> stateMachine;
@Override
public void doQuirk() {
stateMachine.sendEvent("FALLBACK_EVENT");
}
}

View File

@@ -0,0 +1,19 @@
package click.kamil.examples.statemachine.extended.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service("customName")
public class NamedQuirkService implements QuirkService {
@Autowired
@Qualifier("paymentStateMachine")
private StateMachine<String, String> stateMachine;
@Override
public void doQuirk() {
stateMachine.sendEvent("NAMED_EVENT");
}
}

View File

@@ -0,0 +1,6 @@
package click.kamil.examples.statemachine.extended.service;
public interface PaymentService {
void processPayment(String paymentId);
void capturePayment(String paymentId);
}

View File

@@ -0,0 +1,37 @@
package click.kamil.examples.statemachine.extended.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.StateMachinePersister;
import org.springframework.stereotype.Service;
@Service
public class PaymentServiceImpl implements PaymentService {
@Autowired
@Qualifier("paymentStateMachine")
private StateMachine<String, String> stateMachine;
@Autowired
private StateMachinePersister<String, String, String> persister;
@Override
public void processPayment(String paymentId) {
// Send a trigger mapped strictly to PaymentStateMachineConfig
stateMachine.sendEvent("AUTHORIZE");
}
@Override
public void capturePayment(String paymentId) {
try {
persister.restore(stateMachine, paymentId);
} catch (Exception e) {}
org.springframework.messaging.Message<String> msg = org.springframework.messaging.support.MessageBuilder
.withPayload("CAPTURE")
.setHeader("paymentId", paymentId)
.build();
stateMachine.sendEvent(msg);
}
}

View File

@@ -0,0 +1,21 @@
package click.kamil.examples.statemachine.extended.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service
@Primary
public class PrimaryQuirkService implements QuirkService {
@Autowired
@Qualifier("paymentStateMachine")
private StateMachine<String, String> stateMachine;
@Override
public void doQuirk() {
stateMachine.sendEvent("PRIMARY_EVENT");
}
}

View File

@@ -0,0 +1,21 @@
package click.kamil.examples.statemachine.extended.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Profile;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service
@Profile("nonexistent")
public class ProfiledQuirkService implements QuirkService {
@Autowired
@Qualifier("paymentStateMachine")
private StateMachine<String, String> stateMachine;
@Override
public void doQuirk() {
stateMachine.sendEvent("PROFILED_EVENT");
}
}

View File

@@ -0,0 +1,19 @@
package click.kamil.examples.statemachine.extended.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service
public class QualifierQuirkService implements QuirkService {
@Autowired
@Qualifier("paymentStateMachine")
private StateMachine<String, String> stateMachine;
@Override
public void doQuirk() {
stateMachine.sendEvent("QUALIFIER_EVENT");
}
}

View File

@@ -0,0 +1,5 @@
package click.kamil.examples.statemachine.extended.service;
public interface QuirkService {
void doQuirk();
}

View File

@@ -0,0 +1,10 @@
package click.kamil.examples.statemachine.extended.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
public interface BaseController {
@GetMapping("/api/base/{id}")
String processBaseEndpoint(@PathVariable String id);
}

View File

@@ -0,0 +1,28 @@
package click.kamil.examples.statemachine.extended.web;
import click.kamil.examples.statemachine.extended.service.PaymentService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PathVariable;
@RestController
public class PaymentController implements BaseController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Override
public String processBaseEndpoint(@PathVariable String id) {
paymentService.processPayment(id);
return "Started Base Payment: " + id;
}
@PostMapping("/api/payment/{id}/capture")
public String capturePaymentEndpoint(@PathVariable String id) {
paymentService.capturePayment(id);
return "Captured: " + id;
}
}

View File

@@ -0,0 +1,45 @@
package click.kamil.examples.statemachine.extended.web;
import click.kamil.examples.statemachine.extended.service.QuirkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class QuirkController {
@Autowired
private QuirkService quirkService; // Should resolve to PrimaryQuirkService
@Autowired
@Qualifier("customName")
private QuirkService someService; // Should resolve to NamedQuirkService
@Autowired
@Qualifier("qualifierQuirkService")
private QuirkService anotherService; // Should resolve to QualifierQuirkService
@Autowired
private QuirkService fallbackQuirkService; // Should resolve to FallbackQuirkService
@PostMapping("/api/quirk/primary")
public void testPrimary() {
quirkService.doQuirk();
}
@PostMapping("/api/quirk/named")
public void testNamed() {
someService.doQuirk();
}
@PostMapping("/api/quirk/qualifier")
public void testQualifier() {
anotherService.doQuirk();
}
@PostMapping("/api/quirk/fallback")
public void testFallback() {
fallbackQuirkService.doQuirk();
}
}