1 Commits

Author SHA1 Message Date
db58c55b84 attempt to adding spring bean registry 2026-06-20 16:08:17 +02:00
40 changed files with 1103 additions and 3575 deletions

View File

@@ -14,16 +14,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.HeuristicEventMatchingEngine;
public class TransitionLinkerEnricher implements AnalysisEnricher { public class TransitionLinkerEnricher implements AnalysisEnricher {
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
private final EventMatchingEngine matchingEngine = new HeuristicEventMatchingEngine();
@Override @Override
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
if (result.getMetadata() == null || result.getMetadata().getCallChains() == null) { if (result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
@@ -46,10 +38,41 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
List<MatchedTransition> matched = new ArrayList<>(); List<MatchedTransition> matched = new ArrayList<>();
for (Transition t : stateMachineTransitions) { for (Transition t : stateMachineTransitions) {
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) { if (t.getEvent() != null) {
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName(); String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
// Event matches String smEvent = simplify(smEventRaw);
for (State smSourceState : t.getSourceStates()) {
boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") ||
triggerEvent.equals("msg") || triggerEvent.equals("message") ||
triggerEvent.equals("payload");
if (isWildcard) {
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
// We no longer hard-block wildcards without a specific routing context.
// If a project doesn't use standard SM persisters (e.g. restores state manually),
// contextMachineId will be null. We should still link the wildcard to provide SOME visibility,
// rather than completely hiding the endpoint.
}
List<String> polyEvents = tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : java.util.Collections.emptyList();
boolean hasPolyMatch = false;
for (String pe : polyEvents) {
String simplePe = pe;
if (pe.contains(".")) {
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
}
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
hasPolyMatch = true;
break;
}
}
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent)))) {
// Event matches or is a wildcard
for (State smSourceState : t.getSourceStates()) {
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName(); String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
String smSource = simplify(smSourceRaw); String smSource = simplify(smSourceRaw);
if (triggerSource == null || triggerSource.equals(smSource)) { if (triggerSource == null || triggerSource.equals(smSource)) {
@@ -79,6 +102,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
} }
} }
} }
}
if (!matched.isEmpty()) { if (!matched.isEmpty()) {
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build(); CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
@@ -101,7 +125,155 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) { private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName); String beanId = chain.getTriggerPoint() != null ? chain.getTriggerPoint().getStateMachineId() : null;
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();
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 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) { private String simplify(String name) {

View File

@@ -1,11 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.model.Event;
public interface EventMatchingEngine {
/**
* Determines whether the given trigger point is a match for the state machine event.
*/
boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint);
}

View File

@@ -1,53 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.model.Event;
import java.util.List;
public class HeuristicEventMatchingEngine implements EventMatchingEngine {
@Override
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
return false;
}
String triggerEvent = simplify(triggerPoint.getEvent());
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
String smEvent = simplify(smEventRaw);
boolean isWildcard = isWildcardVariable(triggerEvent);
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
boolean hasPolyMatch = false;
for (String pe : polyEvents) {
String simplePe = pe;
if (pe.contains(".")) {
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
}
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
hasPolyMatch = true;
break;
}
}
return hasPolyMatch || smEvent.equals(triggerEvent) || (polyEvents.isEmpty() && isWildcard);
}
private boolean isWildcardVariable(String eventStr) {
return eventStr.equals("event") || eventStr.equals("e") ||
eventStr.equals("msg") || eventStr.equals("message") ||
eventStr.equals("payload");
}
private String simplify(String name) {
if (name == null) return null;
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
if (simplified.isEmpty()) {
simplified = name;
}
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
return simplified;
}
}

View File

@@ -1,7 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
public interface BeanResolutionEngine {
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName);
}

View File

@@ -1,188 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import java.util.HashSet;
import java.util.Set;
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
@Override
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : "";
if (targetVar != null && !targetVar.isEmpty()) {
targetVar = targetVar.toLowerCase();
if (targetVar.endsWith("statemachine")) {
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
if (!prefix.isEmpty()) {
if (simplifiedMachineName.contains(prefix)) {
return true; // Explicit positive match
} else {
return false; // Explicit negative match
}
}
} else if (simplifiedMachineName.contains(targetVar)) {
return true;
}
}
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);
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
hasPositiveMatch = true;
}
if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
hasPositiveMatch = true;
}
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
hasStrongMismatch = true;
}
}
if (hasPositiveMatch) {
return true;
}
if (hasStrongMismatch) {
return false;
}
}
return true;
}
private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
if (smPackage.startsWith(chainPackage) || chainPackage.startsWith(smPackage)) {
return true;
}
// Find deepest common package root
String[] p1 = smPackage.split("\\.");
String[] p2 = chainPackage.split("\\.");
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 the state machine's prefix is part of the shared common package,
// then the state machine represents the parent domain of the caller.
if (smPrefix != null && matchIdx >= 0) {
String lowerPrefix = smPrefix.toLowerCase();
for (int i = 0; i <= matchIdx; i++) {
if (p1[i].toLowerCase().contains(lowerPrefix) || lowerPrefix.contains(p1[i].toLowerCase())) {
return true;
}
}
}
return false;
}
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
String[] p1 = smPackage.split("\\.");
String[] p2 = chainPackage.split("\\.");
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;
}
}
Set<String> smDivergentTerms = new HashSet<>();
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
for (int i = matchIdx + 1; i < p1.length; i++) {
smDivergentTerms.add(p1[i].toLowerCase());
}
Set<String> chainDivergentTerms = new HashSet<>();
if (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
for (int i = matchIdx + 1; i < p2.length; i++) {
chainDivergentTerms.add(p2[i].toLowerCase());
}
if (!smDivergentTerms.isEmpty() && !chainDivergentTerms.isEmpty()) {
Set<String> intersection = new HashSet<>(smDivergentTerms);
intersection.retainAll(chainDivergentTerms);
return intersection.isEmpty();
}
return false;
}
private Set<String> extractDomainTerms(String pkg, String prefix) {
Set<String> terms = new HashSet<>();
if (pkg != null && !pkg.isEmpty()) {
String[] segments = pkg.split("\\.");
// Take segments from index 2 onwards if length > 2
int start = segments.length > 2 ? 2 : 0;
for (int i = start; i < segments.length; i++) {
terms.add(segments[i].toLowerCase());
}
}
if (prefix != null && !prefix.isEmpty()) {
terms.add(prefix.toLowerCase());
}
return terms;
}
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;
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
if (words.length > 0) {
return words[0];
}
return null;
}
}

View File

@@ -1,11 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.model;
import lombok.Data;
import java.util.List;
@Data
public class CallEdge {
private final String targetMethod;
private final List<String> arguments;
}

View File

@@ -21,4 +21,6 @@ public class TriggerPoint {
private final String sourceState; // Optional: if we can determine the expected current state private final String sourceState; // Optional: if we can determine the expected current state
private final int lineNumber; private final int lineNumber;
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism 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

@@ -180,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 { } finally {
visited.remove(fieldId); visited.remove(fieldId);
} }
return null; 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) { private String findValueFromAnnotation(FieldDeclaration fd) {
for (Object mod : fd.modifiers()) { for (Object mod : fd.modifiers()) {
if (mod instanceof Annotation ann) { if (mod instanceof Annotation ann) {

View File

@@ -1,7 +1,6 @@
package click.kamil.springstatemachineexporter.analysis.service; package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain; import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
@@ -12,14 +11,26 @@ import org.eclipse.jdt.core.dom.*;
import java.util.*; import java.util.*;
@Slf4j @Slf4j
public class HeuristicCallGraphEngine implements CallGraphEngine { public class CallGraphBuilder {
private final CodebaseContext context; private final CodebaseContext context;
private final click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver springDependencyResolver;
private final ConstantResolver constantResolver; private final ConstantResolver constantResolver;
private String currentMethodFqn; private String currentMethodFqn;
private Map<String, List<CallEdge>> graph; private Map<String, List<CallEdge>> graph;
public HeuristicCallGraphEngine(CodebaseContext context) { @lombok.Data
private static class CallEdge {
private final String targetMethod;
private final List<String> arguments;
}
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.context = context;
this.springDependencyResolver = springDependencyResolver;
this.constantResolver = new ConstantResolver(); this.constantResolver = new ConstantResolver();
} }
@@ -187,7 +198,16 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
typesToInspect.add(declaredType); typesToInspect.add(declaredType);
} else { } else {
typesToInspect.add(declaredType); typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(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); System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
@@ -535,9 +555,23 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
List<String> impls = context.getImplementations(fallbackTypeFqn); String receiverName = null;
for (String impl : impls) { if (emr.getExpression() instanceof SimpleName sn) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args)); 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));
}
}
} }
} }
} }
@@ -644,21 +678,91 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
if (baseCalled == null) return Collections.emptyList(); if (baseCalled == null) return Collections.emptyList();
List<String> allResolved = new ArrayList<>(); List<String> allResolved = new ArrayList<>();
allResolved.add(baseCalled);
if (baseCalled.contains(".")) { if (baseCalled.contains(".")) {
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
String receiverName = null;
List<String> impls = context.getImplementations(className); if (node.getExpression() instanceof SimpleName sn) {
for (String impl : impls) { receiverName = sn.getIdentifier();
allResolved.add(impl + "." + methodName); } 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; 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) { private String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression(); Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier(); String methodName = node.getName().getIdentifier();
@@ -813,10 +917,25 @@ public class HeuristicCallGraphEngine implements CallGraphEngine {
} }
private boolean isHeuristicMatch(String neighbor, String target) { private boolean isHeuristicMatch(String neighbor, String target) {
if (neighbor.equals(target)) return true;
if (target.endsWith("." + neighbor)) 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; int nIdx = neighbor.lastIndexOf('.');
return simpleNeighbor.equals(simpleTarget); 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) { private TypeDeclaration findEnclosingType(ASTNode node) {

View File

@@ -1,11 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import java.util.List;
public interface CallGraphEngine {
List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
}

View File

@@ -1,162 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.service;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
public class DynamicClasspathResolver {
public List<String> resolveClasspath(Path projectRoot) {
log.info("Attempting dynamic classpath resolution for project at {}", projectRoot);
if (projectRoot == null || !Files.exists(projectRoot)) {
return Collections.emptyList();
}
if (Files.exists(projectRoot.resolve("pom.xml"))) {
return resolveMaven(projectRoot);
} else if (Files.exists(projectRoot.resolve("build.gradle")) || Files.exists(projectRoot.resolve("build.gradle.kts"))) {
return resolveGradle(projectRoot);
}
log.info("No Maven or Gradle configuration found at {}. Proceeding without dynamic classpath.", projectRoot);
return Collections.emptyList();
}
protected List<String> resolveMaven(Path projectRoot) {
log.info("Maven project detected. Resolving dependencies...");
Path cpFile = null;
try {
cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt");
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
if (mvnCmd == null) {
log.warn("Maven executable not found.");
return Collections.emptyList();
}
ProcessBuilder pb = new ProcessBuilder(
mvnCmd,
"-q",
"dependency:build-classpath",
"-DincludeScope=compile",
"-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString()
);
pb.directory(projectRoot.toFile());
runProcess(pb);
if (Files.exists(cpFile)) {
String cpString = Files.readString(cpFile).trim();
if (!cpString.isEmpty()) {
return Arrays.asList(cpString.split(File.pathSeparator));
}
}
} catch (Exception e) {
log.error("Failed to dynamically resolve Maven classpath", e);
} finally {
if (cpFile != null) {
try {
Files.deleteIfExists(cpFile);
} catch (IOException ignored) {}
}
}
return Collections.emptyList();
}
protected List<String> resolveGradle(Path projectRoot) {
log.info("Gradle project detected. Resolving dependencies...");
Path initScript = null;
try {
initScript = Files.createTempFile("state_machine_exporter_init", ".gradle");
String gradleCmd = getCommand(projectRoot, "gradlew", "gradle");
if (gradleCmd == null) {
log.warn("Gradle executable not found.");
return Collections.emptyList();
}
String scriptContent = """
allprojects {
task smePrintClasspath {
doLast {
def cp = []
if (configurations.findByName("compileClasspath")) {
cp = configurations.compileClasspath.files
}
println "SME_CLASSPATH_MARKER:" + cp.join(File.pathSeparator)
}
}
}
""";
Files.writeString(initScript, scriptContent);
ProcessBuilder pb = new ProcessBuilder(
gradleCmd,
"-q",
"-I", initScript.toAbsolutePath().toString(),
"smePrintClasspath"
);
pb.directory(projectRoot.toFile());
String output = runProcessAndGetOutput(pb);
for (String line : output.split("\\r?\\n")) {
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
if (!cpString.isEmpty()) {
return Arrays.asList(cpString.split(File.pathSeparator));
}
}
}
} catch (Exception e) {
log.error("Failed to dynamically resolve Gradle classpath", e);
} finally {
if (initScript != null) {
try {
Files.deleteIfExists(initScript);
} catch (IOException ignored) {}
}
}
return Collections.emptyList();
}
private String getCommand(Path projectRoot, String wrapper, String systemBinary) {
Path wrapperUnix = projectRoot.resolve(wrapper);
Path wrapperWin = projectRoot.resolve(wrapper + ".bat");
boolean isWin = System.getProperty("os.name").toLowerCase().contains("win");
if (isWin && Files.exists(wrapperWin)) return wrapperWin.toAbsolutePath().toString();
if (!isWin && Files.exists(wrapperUnix)) {
// Need absolute path like ./gradlew
return wrapperUnix.toAbsolutePath().toString();
}
return systemBinary;
}
protected void runProcess(ProcessBuilder pb) throws IOException, InterruptedException {
Process p = pb.start();
p.waitFor();
}
protected String runProcessAndGetOutput(ProcessBuilder pb) throws IOException, InterruptedException {
Process p = pb.start();
StringBuilder out = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
out.append(line).append("\n");
}
}
p.waitFor();
return out.toString();
}
}

View File

@@ -18,6 +18,11 @@ public class GenericEventDetector {
private final CodebaseContext context; private final CodebaseContext context;
private final ConstantResolver constantResolver; private final ConstantResolver constantResolver;
private final List<LibraryHint> hints; 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) { public List<TriggerPoint> detect(CompilationUnit cu) {
List<TriggerPoint> triggers = new ArrayList<>(); List<TriggerPoint> triggers = new ArrayList<>();
@@ -142,6 +147,28 @@ public class GenericEventDetector {
String sourceState = extractSourceState(node); 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<>(); List<TriggerPoint> results = new ArrayList<>();
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) { if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
String[] parts = eventValue.substring(9).split(","); String[] parts = eventValue.substring(9).split(",");
@@ -150,10 +177,12 @@ public class GenericEventDetector {
results.add(TriggerPoint.builder() results.add(TriggerPoint.builder()
.event(part.trim()) .event(part.trim())
.sourceState(sourceState) .sourceState(sourceState)
.stateMachineId(stateMachineId)
.className(context.getFqn(type)) .className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer") .methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type))) .sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition())) .lineNumber(cu.getLineNumber(node.getStartPosition()))
.payloadTypes(payloadTypes)
.build()); .build());
} }
} }
@@ -161,10 +190,12 @@ public class GenericEventDetector {
results.add(TriggerPoint.builder() results.add(TriggerPoint.builder()
.event(eventValue) .event(eventValue)
.sourceState(sourceState) .sourceState(sourceState)
.stateMachineId(stateMachineId)
.className(context.getFqn(type)) .className(context.getFqn(type))
.methodName(method != null ? method.getName().getIdentifier() : "initializer") .methodName(method != null ? method.getName().getIdentifier() : "initializer")
.sourceFile(context.getRelativePath(context.getFqn(type))) .sourceFile(context.getRelativePath(context.getFqn(type)))
.lineNumber(cu.getLineNumber(node.getStartPosition())) .lineNumber(cu.getLineNumber(node.getStartPosition()))
.payloadTypes(payloadTypes)
.build()); .build());
} }
@@ -172,97 +203,40 @@ public class GenericEventDetector {
} }
private String extractSourceState(ASTNode node) { private String extractSourceState(ASTNode node) {
ASTNode current = node; ASTNode current = node.getParent();
while (current != null && !(current instanceof MethodDeclaration)) { while (current != null && !(current instanceof MethodDeclaration)) {
ASTNode parent = current.getParent(); if (current instanceof IfStatement ifStmt) {
Expression expr = ifStmt.getExpression();
// 1. Direct parent is IfStatement wrapper (e.g., if (state == X) { sm.sendEvent() }) String state = extractStateFromExpression(expr);
if (parent instanceof IfStatement ifStmt) {
// Only consider if our current node is in the 'then' or 'else' part, not the condition
if (ifStmt.getExpression() != current) {
Expression expr = ifStmt.getExpression();
String state = extractStateFromExpression(expr);
if (state != null) return state;
}
}
// 2. Parent is a Block or SwitchStatement: perform sibling traversal
if (parent instanceof Block block) {
String state = extractStateFromSiblings(current, block.statements());
if (state != null) return state; if (state != null) return state;
} else if (parent instanceof SwitchStatement switchStmt) { } else if (current instanceof SwitchCase switchCase) {
String state = extractStateFromSiblings(current, switchStmt.statements());
if (state != null) return state;
}
current = parent;
}
return null;
}
private String extractStateFromSiblings(ASTNode currentNode, List<?> statements) {
int index = statements.indexOf(currentNode);
if (index <= 0) return null; // No previous siblings
// Walk backwards through siblings
for (int i = index - 1; i >= 0; i--) {
Object stmtObj = statements.get(i);
if (stmtObj instanceof SwitchCase switchCase) {
if (!switchCase.expressions().isEmpty()) { if (!switchCase.expressions().isEmpty()) {
return getSimpleNameString((Expression) switchCase.expressions().get(0)); return getSimpleNameString((Expression) switchCase.expressions().get(0));
} }
} else if (stmtObj instanceof IfStatement ifStmt) { } else if (current instanceof SwitchStatement switchStmt) {
// Guard clause detection: check if the 'then' block has a return/throw // If it's a switch block but we haven't hit a SwitchCase directly (AST hierarchy usually has SwitchCase as siblings of block statements)
if (isGuardClause(ifStmt.getThenStatement())) { // Actually, walking up from the node, we will hit the SwitchStatement, but we need the SwitchCase right before us in the block.
Expression expr = ifStmt.getExpression(); ASTNode parentBlock = current;
// If it's a guard clause, it usually checks `state != EXPECTED`, so the true state *is* EXPECTED // It's complex to walk siblings. A simpler heuristic is to just use IfStatement and SwitchCase (if wrapped correctly).
String state = extractStateFromExpression(expr);
if (state != null) return state;
}
} }
current = current.getParent();
} }
return null; return null;
} }
private boolean isGuardClause(Statement thenStatement) {
if (thenStatement instanceof ReturnStatement || thenStatement instanceof ThrowStatement) {
return true;
}
if (thenStatement instanceof Block block) {
for (Object obj : block.statements()) {
if (obj instanceof ReturnStatement || obj instanceof ThrowStatement) {
return true;
}
}
}
return false;
}
private String extractStateFromExpression(Expression expr) { private String extractStateFromExpression(Expression expr) {
if (expr instanceof InfixExpression infix) { if (expr instanceof InfixExpression infix) {
if (infix.getOperator() == InfixExpression.Operator.EQUALS || infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) { if (infix.getOperator() == InfixExpression.Operator.EQUALS) {
Expression left = infix.getLeftOperand(); Expression left = infix.getLeftOperand();
Expression right = infix.getRightOperand(); Expression right = infix.getRightOperand();
// Usually one is a method call like getState(), the other is an enum
// Ignore null checks entirely if (left instanceof QualifiedName || left instanceof SimpleName && !(right instanceof SimpleName)) {
if (left instanceof NullLiteral || right instanceof NullLiteral) {
return null;
}
// Usually one is a method call like getState() or a variable like `state`
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
// If one is a QualifiedName (enum constant) or StringLiteral, it's likely the state
if (left instanceof QualifiedName || left instanceof StringLiteral || left instanceof FieldAccess) {
return getSimpleNameString(left); return getSimpleNameString(left);
} }
if (right instanceof QualifiedName || right instanceof StringLiteral || right instanceof FieldAccess) { if (right instanceof QualifiedName || right instanceof SimpleName && !(left instanceof SimpleName)) {
return getSimpleNameString(right); return getSimpleNameString(right);
} }
return getSimpleNameString(right); // Fallback
// Fallback
return getSimpleNameString(right);
} }
} else if (expr instanceof MethodInvocation mi) { } else if (expr instanceof MethodInvocation mi) {
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) { if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
@@ -283,6 +257,78 @@ public class GenericEventDetector {
return expr.toString(); 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) { private String extractEventFromMessageBuilder(Expression expr) {
if (expr instanceof CastExpression ce) { if (expr instanceof CastExpression ce) {
return extractEventFromMessageBuilder(ce.getExpression()); return extractEventFromMessageBuilder(ce.getExpression());
@@ -373,6 +419,77 @@ public class GenericEventDetector {
return null; 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) { private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent(); ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) { while (parent != null && !(parent instanceof MethodDeclaration)) {

View File

@@ -1,986 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
@Slf4j
public class JdtCallGraphEngine implements CallGraphEngine {
private final CodebaseContext context;
private final ConstantResolver constantResolver;
private final InjectionPointAnalyzer injectionAnalyzer;
private String currentMethodFqn;
private Map<String, List<CallEdge>> graph;
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
this.context = context;
this.constantResolver = new ConstantResolver();
this.injectionAnalyzer = injectionAnalyzer;
}
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
Map<String, List<CallEdge>> callGraph = buildCallGraph();
List<CallChain> chains = new ArrayList<>();
for (EntryPoint ep : entryPoints) {
String startMethod = ep.getClassName() + "." + ep.getMethodName();
boolean foundAny = false;
for (TriggerPoint tp : triggers) {
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
if (path != null) {
foundAny = true;
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
String contextMachineId = extractContextMachineId(path, callGraph);
chains.add(CallChain.builder()
.entryPoint(ep)
.triggerPoint(resolvedTp)
.methodChain(path)
.contextMachineId(contextMachineId)
.build());
}
}
if (!foundAny && log.isDebugEnabled()) {
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
}
}
return chains;
}
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
if (path.size() < 2) return tp;
String event = tp.getEvent();
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
String currentParamName = event;
String resolvedValue = event;
String methodSuffix = "";
// Extract method calls like .getType() so we can trace the base parameter
int dotIndex = currentParamName.indexOf('.');
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
char nextChar = currentParamName.charAt(dotIndex + 1);
if (Character.isLowerCase(nextChar)) {
methodSuffix = currentParamName.substring(dotIndex);
currentParamName = currentParamName.substring(0, dotIndex);
}
}
// Walk backwards up the call chain
for (int i = path.size() - 1; i > 0; i--) {
String target = path.get(i);
String caller = path.get(i - 1);
// Find parameter index in target method
int paramIndex = getParameterIndex(target, currentParamName);
if (paramIndex < 0) {
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
String tracedVar = traceLocalVariable(target, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
// Extract method calls like .getType() from the traced variable
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
paramIndex = getParameterIndex(target, currentParamName);
}
}
if (paramIndex < 0) {
break; // Parameter name changed or not found, stop tracing
}
// Find the edge from caller to target to get the argument passed
List<CallEdge> edges = callGraph.get(caller);
boolean found = false;
if (edges != null) {
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
if (paramIndex < edge.getArguments().size()) {
String arg = edge.getArguments().get(paramIndex);
if (arg != null) {
// If the argument passed has a method call, extract it
int dotIdx = arg.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
methodSuffix = arg.substring(dotIdx) + methodSuffix;
arg = arg.substring(0, dotIdx);
}
currentParamName = arg;
resolvedValue = arg + methodSuffix;
found = true;
break;
}
}
}
}
}
if (!found) break; // Could not map argument
}
// Final check on the entry method
String entryMethod = path.get(0);
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
if (entryParamIndex < 0) {
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
}
}
List<String> polymorphicEvents = new ArrayList<>();
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
int lastDot = resolvedValue.lastIndexOf('.');
int firstDot = resolvedValue.indexOf('.');
int openParen = resolvedValue.indexOf('(', lastDot);
if (lastDot > 0 && openParen > lastDot) {
String varName = resolvedValue.substring(0, firstDot);
if (varName.contains("(")) {
varName = null;
}
String methodName = resolvedValue.substring(lastDot + 1, openParen);
String declaredType = null;
String sourceMethod = null;
if (varName != null) {
for (String methodFqn : path) {
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: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
List<String> typesToInspect = new ArrayList<>();
if ("inline-instantiation".equals(sourceMethod)) {
typesToInspect.add(declaredType);
} else {
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
}
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
for (String type : typesToInspect) {
Set<String> visited = new HashSet<>();
TypeDeclaration baseTd = context.getTypeDeclaration(type);
CompilationUnit cuToUse = null;
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) baseTd.getRoot();
} else {
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();
}
}
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
for (String constant : constants) {
if (!polymorphicEvents.contains(constant)) {
polymorphicEvents.add(constant);
}
}
}
}
}
}
// 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)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.build();
}
return tp;
}
private int getParameterIndex(String methodFqn, String paramName) {
if (methodFqn == null || !methodFqn.contains(".")) return -1;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
for (int i = 0; i < md.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
if (svd.getName().getIdentifier().equals(paramName)) {
return i;
}
}
}
}
return -1;
}
private String getVariableDeclaredType(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
for (Object pObj : md.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
if (svd.getName().getIdentifier().equals(varName)) {
return svd.getType().toString();
}
}
final String[] foundType = new String[1];
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType().toString();
}
}
return super.visit(node);
}
});
}
return foundType[0];
}
}
return null;
}
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
if (depth > 20) return Collections.emptyList();
String fqn = className + "." + methodName;
if (!visited.add(fqn)) return Collections.emptyList();
List<String> constants = new ArrayList<>();
TypeDeclaration 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);
}
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
Expression retExpr = node.getExpression();
if (retExpr != null) {
boolean handled = false;
if (retExpr instanceof MethodInvocation mi) {
// Follow delegation first
String called = resolveCalledMethod(mi);
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
if (called != null && called.contains(".")) {
if (visited.contains(called)) {
handled = true;
} else {
String cName = called.substring(0, called.lastIndexOf('.'));
String mName = called.substring(called.lastIndexOf('.') + 1);
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
}
if (!handled) {
String val = constantResolver.resolve(retExpr, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
constants.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
constants.add(val);
}
} else if (retExpr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (retExpr instanceof SimpleName sn) {
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
if (!consts.isEmpty()) {
constants.addAll(consts);
} else {
constants.add(sn.toString());
}
}
}
}
return super.visit(node);
}
});
}
}
visited.remove(fqn);
return constants;
}
private String traceLocalVariable(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
final Expression[] initializer = new Expression[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
});
if (initializer[0] != null) {
Expression expr = traceVariable(initializer[0]);
if (expr instanceof MethodInvocation mi) {
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
Expression innerMost = unwrapMethodInvocation(mi, 0);
if (innerMost instanceof MethodInvocation innerMi) {
if (innerMi.getExpression() instanceof SimpleName sn) {
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
}
return innerMi.getName().getIdentifier() + "()";
}
if (innerMost instanceof SimpleName sn) {
return sn.getIdentifier();
}
return innerMost.toString();
}
if (expr instanceof SimpleName sn) {
return sn.getIdentifier();
}
return expr.toString();
}
}
}
return null;
}
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
if (depth > 5) return mi;
if (!mi.arguments().isEmpty()) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1);
}
return arg;
}
return mi;
}
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
for (String node : path) {
List<CallEdge> edges = callGraph.get(node);
if (edges != null) {
for (CallEdge edge : edges) {
String target = edge.getTargetMethod();
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
// Persister signatures usually like: restore(stateMachine, contextObj)
if (edge.getArguments().size() >= 2) {
return edge.getArguments().get(1); // The contextObj / machineId
}
}
}
}
}
return null;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
return (MethodDeclaration) parent;
}
private Map<String, List<CallEdge>> buildCallGraph() {
graph = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration td = findEnclosingType(md);
if (td != null) {
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
String typeName = emr.getExpression().toString();
if ("this".equals(typeName) || "super".equals(typeName)) {
TypeDeclaration td2 = findEnclosingType(node);
if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
}
}
} else {
String fallbackTypeFqn = null;
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
if (binding != null) {
fallbackTypeFqn = binding.getQualifiedName();
} else if (emr.getExpression() instanceof SimpleName sn) {
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
}
if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperMethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration tdOuter = findEnclosingType(md);
if (tdOuter != null) {
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
String methodName = node.getName().getIdentifier();
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
String calledMethod = null;
if (superTd != null) {
calledMethod = resolveMethodInType(superTd, methodName);
}
if (calledMethod == null) {
calledMethod = superFqn + "." + methodName;
}
List<String> args = resolveArguments(node.arguments());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
}
}
}
return super.visit(node);
}
});
}
return graph;
}
private List<String> resolveArguments(List<?> astArguments) {
List<String> args = new ArrayList<>();
for (Object argObj : astArguments) {
Expression expr = (Expression) argObj;
// Extract from lambda
if (expr instanceof LambdaExpression le) {
ASTNode body = le.getBody();
if (body instanceof Expression bodyExpr) {
expr = bodyExpr;
} else if (body instanceof Block block) {
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
if (expr instanceof ExpressionMethodReference emr) {
TypeDeclaration td = findEnclosingType(expr);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
}
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
args.add(val);
}
return args;
}
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
String baseCalled = resolveCalledMethod(node);
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);
}
}
return allResolved;
}
private String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier();
if (receiver == null) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return resolveMethodInType(td, methodName);
}
return null;
}
if (injectionAnalyzer != null) {
IBinding nameBinding = null;
if (receiver instanceof SimpleName sn) {
nameBinding = sn.resolveBinding();
} else if (receiver instanceof FieldAccess fa) {
nameBinding = fa.resolveFieldBinding();
}
if (nameBinding instanceof IVariableBinding varBinding) {
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
if (concreteFqn != null) {
return concreteFqn + "." + methodName;
}
}
}
ITypeBinding binding = receiver.resolveTypeBinding();
if (binding != null) {
return binding.getErasure().getQualifiedName() + "." + methodName;
}
if (receiver instanceof ThisExpression) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return context.getFqn(td) + "." + methodName;
}
}
if (receiver instanceof SimpleName sn) {
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
if (fallbackTypeFqn != null) {
return fallbackTypeFqn + "." + methodName;
}
String receiverName = sn.getIdentifier();
return receiverName + "." + methodName;
}
return null;
}
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
String varName = receiverNameNode.getIdentifier();
// 1. Check local variables in enclosing method
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
if (enclosingMethod != null) {
// Check parameters
for (Object paramObj : enclosingMethod.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
if (svd.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(svd.getType(), receiverNameNode);
}
}
// Check method body (local variables)
if (enclosingMethod.getBody() != null) {
Type[] foundType = new Type[1];
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType();
}
}
return super.visit(node);
}
});
if (foundType[0] != null) {
return resolveTypeToFqn(foundType[0], receiverNameNode);
}
}
}
// 2. Check fields in enclosing class
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
if (enclosingType != null) {
for (FieldDeclaration field : enclosingType.getFields()) {
for (Object fragObj : field.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(field.getType(), receiverNameNode);
}
}
}
}
return null;
}
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
} else {
simpleName = type.toString();
}
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
return simpleName;
}
private String resolveMethodInType(TypeDeclaration td, String methodName) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
TypeDeclaration declaringTd = findEnclosingType(md);
return context.getFqn(declaringTd) + "." + methodName;
}
return null;
}
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
if (start.equals(target)) return new ArrayList<>(List.of(start));
if (!visited.add(start)) return null; // Path-scoped cycle detection
List<CallEdge> neighbors = graph.get(start);
if (neighbors != null) {
for (CallEdge edge : neighbors) {
String neighbor = edge.getTargetMethod();
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
visited.remove(start);
return new ArrayList<>(List.of(start, target));
}
List<String> path = findPath(neighbor, target, graph, visited);
if (path != null) {
path.add(0, start);
visited.remove(start);
return path;
}
}
}
if (log.isDebugEnabled()) {
log.debug("Path search dead-end at {} when looking for {}", start, target);
}
visited.remove(start);
return null;
}
private boolean isHeuristicMatch(String neighbor, String target) {
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);
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private Expression traceVariable(Expression expr) {
if (expr instanceof SimpleName sn) {
String varName = sn.getIdentifier();
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
if (enclosingMethod != null) {
final Expression[] initializer = new Expression[1];
enclosingMethod.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
});
if (initializer[0] != null) {
return traceVariable(initializer[0]);
}
}
}
return expr;
}
private 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

@@ -4,14 +4,13 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.CompilationUnit; 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.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -25,28 +24,26 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
private final GenericEventDetector eventDetector; private final GenericEventDetector eventDetector;
private final SpringMvcDetector mvcDetector; private final SpringMvcDetector mvcDetector;
private final MessagingDetector messagingDetector; private final MessagingDetector messagingDetector;
private final CallGraphEngine callGraphEngine; private final CallGraphBuilder callGraphBuilder;
private final LifecycleDetector lifecycleDetector; private final LifecycleDetector lifecycleDetector;
private final InterceptorDetector interceptorDetector; private final InterceptorDetector interceptorDetector;
private final SpringComponentDetector componentDetector; private final SpringComponentDetector componentDetector;
private final SpringBeanRegistry springBeanRegistry;
private final SpringDependencyResolver springDependencyResolver;
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) { public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
this.context = context; this.context = context;
this.rootDir = rootDir; 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.mvcDetector = new SpringMvcDetector(context, constantResolver);
this.messagingDetector = new MessagingDetector(context); this.messagingDetector = new MessagingDetector(context);
this.callGraphBuilder = new CallGraphBuilder(context, springDependencyResolver);
SpringBeanRegistry registry = new SpringBeanRegistry();
SpringContextScanner scanner = new SpringContextScanner(registry);
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(scanner);
}
SpringDependencyResolver dependencyResolver = new SpringDependencyResolver(registry);
InjectionPointAnalyzer injectionAnalyzer = new InjectionPointAnalyzer(dependencyResolver);
this.callGraphEngine = new JdtCallGraphEngine(context, injectionAnalyzer);
this.lifecycleDetector = new LifecycleDetector(context); this.lifecycleDetector = new LifecycleDetector(context);
this.interceptorDetector = new InterceptorDetector(context); this.interceptorDetector = new InterceptorDetector(context);
this.componentDetector = new SpringComponentDetector(context); this.componentDetector = new SpringComponentDetector(context);
@@ -83,7 +80,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
@Override @Override
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) { public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size()); log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
return callGraphEngine.findChains(entryPoints, triggers); return callGraphBuilder.findChains(entryPoints, triggers);
} }
@Override @Override

View File

@@ -1,83 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import java.util.List;
public class InjectionPointAnalyzer {
private final SpringDependencyResolver resolver;
public InjectionPointAnalyzer(SpringDependencyResolver resolver) {
this.resolver = resolver;
}
/**
* Resolves the Spring bean injected into the given variable.
*
* @param variableBinding The binding of the field or parameter being injected.
* @return The FQN of the resolved concrete bean class, or null if unresolved.
*/
public String resolveInjectedBeanFqn(IVariableBinding variableBinding) {
if (variableBinding == null || variableBinding.getType() == null) {
return null;
}
String requiredTypeFqn = variableBinding.getType().getQualifiedName();
String injectionName = variableBinding.getName();
String qualifier = extractQualifier(variableBinding);
List<SpringBean> resolvedBeans = resolver.resolve(requiredTypeFqn, qualifier, injectionName);
if (resolvedBeans != null && resolvedBeans.size() == 1) {
return resolvedBeans.get(0).getTypeFqn();
}
return null;
}
private String extractQualifier(IVariableBinding binding) {
String qualifier = getQualifierValue(binding.getAnnotations());
if (qualifier != null) {
return qualifier;
}
if (binding.isField()) {
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
for (org.eclipse.jdt.core.dom.IMethodBinding method : declaringClass.getDeclaredMethods()) {
if (method.isConstructor()) {
for (int i = 0; i < method.getParameterTypes().length; i++) {
org.eclipse.jdt.core.dom.ITypeBinding paramType = method.getParameterTypes()[i];
try {
IAnnotationBinding[] paramAnns = method.getParameterAnnotations(i);
String paramQual = getQualifierValue(paramAnns);
if (paramQual != null && paramType.getErasure().isEqualTo(binding.getType().getErasure())) {
return paramQual;
}
} catch (Exception e) {
// Ignore
}
}
}
}
}
}
return null;
}
private String getQualifierValue(IAnnotationBinding[] annotations) {
for (IAnnotationBinding ann : annotations) {
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
return (String) pair.getValue();
}
}
}
}
return null;
}
}

View File

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

View File

@@ -1,14 +1,253 @@
package click.kamil.springstatemachineexporter.analysis.spring; package click.kamil.springstatemachineexporter.analysis.spring;
import java.util.ArrayList; import click.kamil.springstatemachineexporter.ast.common.AstUtils;
import java.util.Collections; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.List; import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class SpringBeanRegistry { public class SpringBeanRegistry {
private final CodebaseContext context;
private final List<SpringBean> beans = new ArrayList<>(); private final List<SpringBean> beans = new ArrayList<>();
public void addBean(SpringBean bean) { private static final Set<String> STEREOTYPES = Set.of(
beans.add(bean); "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() { public List<SpringBean> getBeans() {

View File

@@ -1,179 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import org.eclipse.jdt.core.dom.*;
import java.util.HashSet;
import java.util.Set;
public class SpringContextScanner extends ASTVisitor {
private final SpringBeanRegistry registry;
public SpringContextScanner(SpringBeanRegistry registry) {
this.registry = registry;
}
@Override
public boolean visit(TypeDeclaration node) {
ITypeBinding typeBinding = node.resolveBinding();
if (typeBinding == null) return true;
if (isSpringComponent(typeBinding)) {
SpringBean bean = SpringBean.builder()
.typeFqn(typeBinding.getQualifiedName())
.assignableTypes(getAssignableTypes(typeBinding))
.isPrimary(hasAnnotation(typeBinding, "org.springframework.context.annotation.Primary"))
.qualifiers(extractQualifiers(typeBinding))
.declaringClassFqn(typeBinding.getQualifiedName())
.build();
// Default bean name is uncapitalized simple name
String defaultName = Character.toLowerCase(node.getName().getIdentifier().charAt(0)) + node.getName().getIdentifier().substring(1);
// Check if @Component or @Service specifies a name (e.g., @Service("myService"))
String explicitName = extractStereotypeValue(typeBinding);
if (explicitName != null && !explicitName.isEmpty()) {
bean.getBeanNames().add(explicitName);
} else {
bean.getBeanNames().add(defaultName);
}
registry.addBean(bean);
}
return true;
}
@Override
public boolean visit(MethodDeclaration node) {
IMethodBinding methodBinding = node.resolveBinding();
if (methodBinding == null) return true;
if (hasAnnotation(methodBinding, "org.springframework.context.annotation.Bean")) {
ITypeBinding returnType = methodBinding.getReturnType();
if (returnType != null) {
SpringBean bean = SpringBean.builder()
.typeFqn(returnType.getQualifiedName())
.assignableTypes(getAssignableTypes(returnType))
.isPrimary(hasAnnotation(methodBinding, "org.springframework.context.annotation.Primary"))
.qualifiers(extractQualifiers(methodBinding))
.declaringClassFqn(methodBinding.getDeclaringClass().getQualifiedName())
.factoryMethodName(node.getName().getIdentifier())
.build();
// Default bean name is method name
String defaultName = node.getName().getIdentifier();
// Check if @Bean specifies a name (e.g., @Bean("myBean"))
String explicitName = extractBeanName(methodBinding);
if (explicitName != null && !explicitName.isEmpty()) {
bean.getBeanNames().add(explicitName);
} else {
bean.getBeanNames().add(defaultName);
}
registry.addBean(bean);
}
}
return true;
}
private boolean isSpringComponent(ITypeBinding binding) {
return hasMetaAnnotation(binding, "org.springframework.stereotype.Component") ||
hasMetaAnnotation(binding, "org.springframework.web.bind.annotation.RestController");
}
private boolean hasMetaAnnotation(ITypeBinding binding, String targetFqn) {
return checkMetaAnnotation(binding.getAnnotations(), targetFqn, new HashSet<>());
}
private boolean checkMetaAnnotation(IAnnotationBinding[] annotations, String targetFqn, Set<String> visited) {
for (IAnnotationBinding ann : annotations) {
ITypeBinding annType = ann.getAnnotationType();
if (annType != null) {
String fqn = annType.getQualifiedName();
if (fqn.equals(targetFqn)) return true;
// Avoid circular meta-annotations (e.g. Documented -> Documented)
if (visited.add(fqn)) {
if (checkMetaAnnotation(annType.getAnnotations(), targetFqn, visited)) {
return true;
}
}
}
}
return false;
}
private boolean hasAnnotation(IBinding binding, String annotationFqn) {
IAnnotationBinding[] annotations = binding.getAnnotations();
for (IAnnotationBinding ann : annotations) {
if (ann.getAnnotationType() != null && annotationFqn.equals(ann.getAnnotationType().getQualifiedName())) {
return true;
}
}
return false;
}
private Set<String> getAssignableTypes(ITypeBinding binding) {
Set<String> types = new HashSet<>();
collectAssignableTypes(binding, types);
return types;
}
private void collectAssignableTypes(ITypeBinding binding, Set<String> types) {
if (binding == null) return;
types.add(binding.getQualifiedName());
types.add(binding.getErasure().getQualifiedName()); // Handles generics (e.g., List<String> -> List)
collectAssignableTypes(binding.getSuperclass(), types);
for (ITypeBinding iface : binding.getInterfaces()) {
collectAssignableTypes(iface, types);
}
}
private Set<String> extractQualifiers(IBinding binding) {
Set<String> qualifiers = new HashSet<>();
for (IAnnotationBinding ann : binding.getAnnotations()) {
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
qualifiers.add((String) pair.getValue());
}
}
}
}
return qualifiers;
}
private String extractStereotypeValue(ITypeBinding binding) {
for (IAnnotationBinding ann : binding.getAnnotations()) {
// Check if annotation itself is meta-annotated with @Component
if (checkMetaAnnotation(new IAnnotationBinding[]{ann}, "org.springframework.stereotype.Component", new HashSet<>()) ||
"org.springframework.stereotype.Component".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
return (String) pair.getValue();
}
}
}
}
return null;
}
private String extractBeanName(IMethodBinding binding) {
for (IAnnotationBinding ann : binding.getAnnotations()) {
if ("org.springframework.context.annotation.Bean".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof String) {
return (String) pair.getValue();
} else if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof Object[]) {
Object[] vals = (Object[]) pair.getValue();
if (vals.length > 0 && vals[0] instanceof String) {
return (String) vals[0];
}
}
}
}
}
return null;
}
}

View File

@@ -1,37 +1,48 @@
package click.kamil.springstatemachineexporter.analysis.spring; package click.kamil.springstatemachineexporter.analysis.spring;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class SpringDependencyResolver { public class SpringDependencyResolver {
private final CodebaseContext context;
private final SpringBeanRegistry registry; private final SpringBeanRegistry registry;
public SpringDependencyResolver(SpringBeanRegistry registry) { public SpringDependencyResolver(CodebaseContext context, SpringBeanRegistry registry) {
this.context = context;
this.registry = registry; this.registry = registry;
} }
/** /**
* Resolves the exact SpringBean that would be injected for a given injection point. * Resolves the exact SpringBean that would be injected for a given injection point.
* *
* @param requiredTypeFqn The exact FQN of the type requested at the injection point. * @param requiredTypeFqn The FQN of the type being injected.
* @param qualifier The value of the @Qualifier annotation on the injection point (if any). * @param qualifier The value of the @Qualifier annotation (if any).
* @param injectionName The name of the field or parameter being injected (used as fallback). * @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. * @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) { public List<SpringBean> resolve(String requiredTypeFqn, String qualifier, String injectionName) {
if (requiredTypeFqn == null) return new ArrayList<>(); if (requiredTypeFqn == null) return new ArrayList<>();
// 1. Filter by Type (Exact FQN or Assignable Type) // 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() List<SpringBean> candidates = registry.getBeans().stream()
.filter(bean -> requiredTypeFqn.equals(bean.getTypeFqn()) || bean.getAssignableTypes().contains(requiredTypeFqn)) .filter(bean -> acceptableTypes.contains(bean.getTypeFqn()))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (candidates.isEmpty() || candidates.size() == 1) { if (candidates.isEmpty() || candidates.size() == 1) {
return candidates; return candidates;
} }
// 2. Filter by Qualifier (if provided) // 3. Filter by Qualifier (if provided)
if (qualifier != null && !qualifier.isEmpty()) { if (qualifier != null && !qualifier.isEmpty()) {
List<SpringBean> qualifiedCandidates = candidates.stream() List<SpringBean> qualifiedCandidates = candidates.stream()
.filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier)) .filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier))
@@ -43,7 +54,7 @@ public class SpringDependencyResolver {
} }
} }
// 3. Filter by @Primary // 4. Filter by @Primary
List<SpringBean> primaryCandidates = candidates.stream() List<SpringBean> primaryCandidates = candidates.stream()
.filter(SpringBean::isPrimary) .filter(SpringBean::isPrimary)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -51,7 +62,7 @@ public class SpringDependencyResolver {
return primaryCandidates; return primaryCandidates;
} }
// 4. Fallback to Injection Name (field name or parameter name) // 5. Fallback to Injection Name (field name or parameter name)
if (injectionName != null && !injectionName.isEmpty()) { if (injectionName != null && !injectionName.isEmpty()) {
List<SpringBean> namedCandidates = candidates.stream() List<SpringBean> namedCandidates = candidates.stream()
.filter(bean -> bean.getBeanNames().contains(injectionName)) .filter(bean -> bean.getBeanNames().contains(injectionName))

View File

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

View File

@@ -57,9 +57,6 @@ public class ExporterCommand implements Callable<Integer> {
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false") @Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
private boolean debug; private boolean debug;
@Option(names = {"--resolve-classpath"}, description = "Automatically run Maven/Gradle to resolve full classpath dependencies for perfect AST bindings.", defaultValue = "false")
private boolean resolveClasspath;
@Override @Override
public Integer call() throws Exception { public Integer call() throws Exception {
var out = spec.commandLine().getOut(); var out = spec.commandLine().getOut();
@@ -90,7 +87,7 @@ public class ExporterCommand implements Callable<Integer> {
if (jsonFile != null) { if (jsonFile != null) {
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat); exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
} else { } else {
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, resolveClasspath); exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, eventFormat, stateFormat);
} }
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath())); out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
return 0; return 0;

View File

@@ -45,6 +45,18 @@ public class ExportService {
new EntryPointEnricher(), new EntryPointEnricher(),
new PropertyEnricher(), new PropertyEnricher(),
new CallChainEnricher(), 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() new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
))); )));
} }
@@ -53,22 +65,22 @@ public class ExportService {
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory"); public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false); runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
} }
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException { public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false); runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
} }
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException { public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false); runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
} }
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException { public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat, false); runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat);
} }
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean resolveClasspath) throws IOException { public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.setActiveProfiles(activeProfiles); context.setActiveProfiles(activeProfiles);
@@ -94,18 +106,6 @@ public class ExportService {
.collect(Collectors.toList()); .collect(Collectors.toList());
log.info("Setting JDT sourcepath: {}", sourcepaths); log.info("Setting JDT sourcepath: {}", sourcepaths);
context.setSourcepath(sourcepaths); context.setSourcepath(sourcepaths);
if (resolveClasspath) {
click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver classpathResolver = new click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver();
List<String> dynamicClasspath = classpathResolver.resolveClasspath(projectRoot);
if (!dynamicClasspath.isEmpty()) {
context.setClasspath(dynamicClasspath);
log.info("Injected {} external JARs into JDT ASTParser", dynamicClasspath.size());
}
} else {
log.info("Dynamic classpath resolution disabled. Use --resolve-classpath to enable.");
}
context.setResolveBindings(true); context.setResolveBindings(true);
Path hintsFile = inputDir.resolve("hints.json"); Path hintsFile = inputDir.resolve("hints.json");

View File

@@ -4,7 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine; import click.kamil.springstatemachineexporter.analysis.service.CallGraphBuilder;
import click.kamil.springstatemachineexporter.analysis.service.GenericEventDetector; import click.kamil.springstatemachineexporter.analysis.service.GenericEventDetector;
import click.kamil.springstatemachineexporter.analysis.service.SpringMvcDetector; import click.kamil.springstatemachineexporter.analysis.service.SpringMvcDetector;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
@@ -76,7 +76,7 @@ public class InheritedEventDetectionTest {
assertThat(tp.getEvent()).isEqualTo("event"); assertThat(tp.getEvent()).isEqualTo("event");
// Build Call Chains // Build Call Chains
HeuristicCallGraphEngine callGraphBuilder = new HeuristicCallGraphEngine(context); CallGraphBuilder callGraphBuilder = new CallGraphBuilder(context);
List<CallChain> chains = callGraphBuilder.findChains(entryPoints, triggers); List<CallChain> chains = callGraphBuilder.findChains(entryPoints, triggers);
assertThat(chains).describedAs("Should link ChildController.trigger to BaseController.send").hasSize(1); assertThat(chains).describedAs("Should link ChildController.trigger to BaseController.send").hasSize(1);

View File

@@ -145,36 +145,6 @@ class TransitionLinkerEnricherTest {
@Test
void shouldMatchBaseEventDespiteScrapedPolyEvents() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
// State machine uses the base event generically
t1.setEvent(Event.of("BaseEvent", "BaseEvent"));
// Trigger point uses "BaseEvent", but polyEvents scraped some implementation/constants
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("BaseEvent")
.polymorphicEvents(List.of("EVENT_A", "EVENT_B"))
.build())
.contextMachineId("testMachine")
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
// It should match because smEvent matches triggerEvent directly, overriding the failed poly search
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
@Test @Test
void shouldNotMatchArbitraryGetXMethodWildcard() { void shouldNotMatchArbitraryGetXMethodWildcard() {
Transition t1 = new Transition(); Transition t1 = new Transition();

View File

@@ -1,103 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
@Tag("heuristic_bean_resolution")
public class HeuristicBeanResolutionEngineTest {
private final HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
@Test
public void testDeepPackageDivergenceMismatch() {
// This is the bug case: sharing a deep root, but diverging into different domains
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.corp.division.project.orders.OrderService.pay()"))
.build();
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// They share com.acme.corp.division.project, but diverge into orders vs payments
// This should be a strong mismatch, so it should return false
assertFalse(result, "Deep package divergence into different domains should be rejected");
}
@Test
public void testSameDomainMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.ecommerce.orders.OrderService.process()"))
.build();
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Same exact domain, should match
assertTrue(result, "Same domain should be accepted");
}
@Test
public void testSubPackageMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.ecommerce.orders.impl.OrderServiceImpl.process()"))
.build();
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Subpackage should match
assertTrue(result, "Sub-packages of the same domain should be accepted");
}
@Test
public void testSharedDomainTermMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.orders.web.OrderController.submit()"))
.build();
String machineName = "com.acme.orders.service.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Different subpackages, but they share the "order" domain term and common prefix
assertTrue(result, "Divergent packages that share a domain term should be accepted");
}
@Test
public void testExplicitTargetVariableMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.common.GlobalTrigger.fire()"))
.contextMachineId("paymentStateMachine") // Explicitly targets payment
.build();
String machineName = "com.acme.corp.payments.PaymentStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Mismatched domains, but explicit variable targeting works
assertTrue(result, "Explicit variable target matching the machine name should be accepted");
}
@Test
public void testExplicitTargetVariableMismatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.common.GlobalTrigger.fire()"))
.contextMachineId("paymentStateMachine") // Explicitly targets payment
.build();
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Explicit variable targeting completely conflicts
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
}
}

View File

@@ -12,11 +12,9 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis") class CallGraphBuilderTest {
class HeuristicCallGraphEngineTest {
@Test @Test
void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException { void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException {
@@ -45,7 +43,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderService") .className("com.example.OrderService")
@@ -96,7 +94,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.PersisterService") .className("com.example.PersisterService")
@@ -148,7 +146,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -202,7 +200,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -270,7 +268,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -324,7 +322,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -394,7 +392,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -440,7 +438,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -491,7 +489,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -549,7 +547,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -599,7 +597,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -652,7 +650,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -697,7 +695,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -749,7 +747,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -795,7 +793,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -839,7 +837,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -889,7 +887,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -935,7 +933,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder() EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController") .className("com.example.OrderController")
@@ -986,7 +984,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("processOrderEvent").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("processOrderEvent").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("updateOrderState").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("updateOrderState").event("event").build();
@@ -1019,7 +1017,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("createOrder").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("createOrder").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build();
@@ -1075,7 +1073,7 @@ class HeuristicCallGraphEngineTest {
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.controller.EventController").methodName("handleEvent").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.controller.EventController").methodName("handleEvent").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.service.EventService").methodName("process").event("type").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.service.EventService").methodName("process").event("type").build();
@@ -1117,7 +1115,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1151,7 +1149,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1183,7 +1181,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1220,7 +1218,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1257,7 +1255,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1292,7 +1290,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1329,7 +1327,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1363,7 +1361,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1399,7 +1397,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1433,7 +1431,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1471,7 +1469,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1512,7 +1510,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1550,7 +1548,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1589,7 +1587,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1632,7 +1630,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1674,7 +1672,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1715,7 +1713,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1756,7 +1754,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1799,7 +1797,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
@@ -1844,7 +1842,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build(); EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build(); EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
@@ -1889,7 +1887,7 @@ class HeuristicCallGraphEngineTest {
Files.writeString(tempDir.resolve("OrderConfig.java"), source); Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext(); CodebaseContext context = new CodebaseContext();
context.scan(tempDir); context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();

View File

@@ -1,78 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.service;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("dynamic_classpath_resolver")
class DynamicClasspathResolverTest {
@Test
void shouldResolveMavenClasspathDynamically(@TempDir Path tempDir) throws IOException {
// Create dummy pom.xml
Files.createFile(tempDir.resolve("pom.xml"));
// Mock the resolver so it doesn't actually run maven, but writes the expected output file
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
@Override
protected void runProcess(ProcessBuilder pb) throws IOException {
// Pretend maven executed and generated the cp.txt
String cp = "/fake/m2/repo/spring-core.jar" + File.pathSeparator + "/fake/m2/repo/spring-context.jar";
String outputFileArg = pb.command().stream()
.filter(arg -> arg.startsWith("-Dmdep.outputFile="))
.findFirst()
.orElseThrow();
Path outputFile = Path.of(outputFileArg.substring("-Dmdep.outputFile=".length()));
Files.writeString(outputFile, cp);
}
};
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).containsExactly(
"/fake/m2/repo/spring-core.jar",
"/fake/m2/repo/spring-context.jar"
);
}
@Test
void shouldResolveGradleClasspathDynamically(@TempDir Path tempDir) throws IOException {
// Create dummy build.gradle
Files.createFile(tempDir.resolve("build.gradle"));
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
@Override
protected String runProcessAndGetOutput(ProcessBuilder pb) {
// Pretend gradle executed and printed the marker
String cp = "/fake/gradle/caches/spring-core.jar" + File.pathSeparator + "/fake/gradle/caches/spring-context.jar";
return "Some noisy gradle output\n" +
"SME_CLASSPATH_MARKER:" + cp + "\n" +
"BUILD SUCCESSFUL\n";
}
};
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).containsExactly(
"/fake/gradle/caches/spring-core.jar",
"/fake/gradle/caches/spring-context.jar"
);
}
@Test
void shouldReturnEmptyListIfNoBuildFileFound(@TempDir Path tempDir) {
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).isEmpty();
}
}

View File

@@ -1,132 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("control_flow_sibling_avoidance")
class GenericEventDetectorControlFlowTest {
@Test
void shouldDetectStateFromDirectIfWrapper(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
private StateMachine sm;
public void processOrder(OrderState state) {
if (state == OrderState.PENDING) {
sm.sendEvent(OrderEvent.PAY);
}
}
}
class StateMachine {
public void sendEvent(OrderEvent e) {}
}
enum OrderState { PENDING }
enum OrderEvent { PAY }
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
assertThat(triggers).hasSize(1);
TriggerPoint tp = triggers.get(0);
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
assertThat(tp.getSourceState()).isEqualTo("PENDING");
}
@Test
void shouldDetectStateFromGuardClauseSibling(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
private StateMachine sm;
public void processOrder(OrderState state) {
if (state != OrderState.PENDING) {
return;
}
sm.sendEvent(OrderEvent.PAY);
}
}
class StateMachine {
public void sendEvent(OrderEvent e) {}
}
enum OrderState { PENDING }
enum OrderEvent { PAY }
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
assertThat(triggers).hasSize(1);
TriggerPoint tp = triggers.get(0);
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
assertThat(tp.getSourceState()).isEqualTo("PENDING");
}
@Test
void shouldDetectStateFromSwitchCaseSibling(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
private StateMachine sm;
public void processOrder(OrderState state) {
switch (state) {
case PENDING:
sm.sendEvent(OrderEvent.PAY);
break;
case PAID:
sm.sendEvent(OrderEvent.SHIP);
break;
}
}
}
class StateMachine {
public void sendEvent(OrderEvent e) {}
}
enum OrderState { PENDING, PAID }
enum OrderEvent { PAY, SHIP }
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
assertThat(triggers).hasSize(2);
TriggerPoint payTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.PAY")).findFirst().get();
assertThat(payTrigger.getSourceState()).isEqualTo("PENDING");
TriggerPoint shipTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.SHIP")).findFirst().get();
assertThat(shipTrigger.getSourceState()).isEqualTo("PAID");
}
}

View File

@@ -1,138 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class JdtCallGraphEngineIntegrationTest {
private CodebaseContext context;
private SpringBeanRegistry registry;
private JdtCallGraphEngine engine;
@BeforeEach
void setUp() throws IOException {
Path projectRoot = Path.of("../state_machines/extended_analysis_sample").toAbsolutePath().normalize();
context = new CodebaseContext();
context.setProjectRoot(projectRoot);
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> cp = resolver.resolveClasspath(projectRoot);
if (!cp.isEmpty()) {
context.setClasspath(cp);
}
context.setResolveBindings(true);
context.scan(Set.of(projectRoot), Collections.emptySet());
registry = new SpringBeanRegistry();
SpringContextScanner scanner = new SpringContextScanner(registry);
for (var cu : context.getCompilationUnits()) {
cu.accept(scanner);
}
SpringDependencyResolver dependencyResolver = new SpringDependencyResolver(registry);
InjectionPointAnalyzer injectionAnalyzer = new InjectionPointAnalyzer(dependencyResolver);
engine = new JdtCallGraphEngine(context, injectionAnalyzer);
}
@Test
void shouldResolvePrimaryBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testPrimary")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.PrimaryQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
assertThat(chains).isNotEmpty();
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary",
"click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk"
);
}
@Test
void shouldResolveQualifierBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testQualifier")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.QualifierQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
assertThat(chains).isNotEmpty();
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier",
"click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk"
);
}
@Test
void shouldResolveNamedBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testNamed")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.NamedQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
assertThat(chains).isNotEmpty();
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testNamed",
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
);
}
@Test
void shouldResolveFallbackBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testFallback")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.FallbackQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
assertThat(chains).isNotEmpty();
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testFallback",
"click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk"
);
}
}

View File

@@ -1,89 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class SpringContextScannerIntegrationTest {
private CodebaseContext context;
private SpringBeanRegistry registry;
@BeforeEach
void setUp() throws IOException {
Path projectRoot = Path.of("../state_machines/extended_analysis_sample").toAbsolutePath().normalize();
context = new CodebaseContext();
context.setProjectRoot(projectRoot);
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> cp = resolver.resolveClasspath(projectRoot);
if (!cp.isEmpty()) {
context.setClasspath(cp);
}
context.setResolveBindings(true);
context.scan(Set.of(projectRoot), Collections.emptySet());
registry = new SpringBeanRegistry();
SpringContextScanner scanner = new SpringContextScanner(registry);
for (var cu : context.getCompilationUnits()) {
cu.accept(scanner);
}
}
@Test
void shouldFindStereotypeBeans() {
List<SpringBean> beans = registry.getBeans();
// PrimaryQuirkService
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.service.PrimaryQuirkService");
assertThat(bean.isPrimary()).isTrue();
assertThat(bean.getAssignableTypes()).contains("click.kamil.examples.statemachine.extended.service.QuirkService");
assertThat(bean.getBeanNames()).contains("primaryQuirkService");
});
// QualifierQuirkService
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.service.QualifierQuirkService");
assertThat(bean.isPrimary()).isFalse();
assertThat(bean.getBeanNames()).contains("qualifierQuirkService");
});
// NamedQuirkService (should have explicit name via @Service("customName"))
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.service.NamedQuirkService");
assertThat(bean.getBeanNames()).contains("customName");
});
// PaymentController (should be detected via @RestController)
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.web.PaymentController");
});
}
@Test
void shouldFindMethodBeans() {
List<SpringBean> beans = registry.getBeans();
// Beans created via @Bean
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("java.lang.String");
assertThat(bean.getFactoryMethodName()).isEqualTo("myStringBean");
assertThat(bean.getBeanNames()).contains("customMockBean");
assertThat(bean.getDeclaringClassFqn()).isEqualTo("click.kamil.examples.statemachine.extended.config.MockBeanConfig");
});
}
}

View File

@@ -1,95 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class SpringDependencyResolverTest {
private SpringBeanRegistry registry;
private SpringDependencyResolver resolver;
@BeforeEach
void setUp() {
registry = new SpringBeanRegistry();
resolver = new SpringDependencyResolver(registry);
}
@Test
void shouldResolveSingleMatchByType() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.PaymentServiceImpl")
.assignableTypes(Set.of("com.example.PaymentService"))
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "paymentService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.PaymentServiceImpl");
}
@Test
void shouldResolveByPrimaryWhenMultipleImplementationsExist() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.NormalService")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(false)
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.PrimaryService")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(true)
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "paymentService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.PrimaryService");
}
@Test
void shouldResolveByQualifierWhenMultipleImplementationsExist() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceA")
.assignableTypes(Set.of("com.example.PaymentService"))
.qualifiers(Set.of("serviceA"))
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceB")
.assignableTypes(Set.of("com.example.PaymentService"))
.qualifiers(Set.of("serviceB"))
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", "serviceB", "paymentService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.ServiceB");
}
@Test
void shouldResolveByInjectionNameFallback() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceA")
.assignableTypes(Set.of("com.example.PaymentService"))
.beanNames(Set.of("serviceA"))
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceB")
.assignableTypes(Set.of("com.example.PaymentService"))
.beanNames(Set.of("specialService"))
.build());
// Qualifier is null, Primary is false. Should match by variable name "specialService"
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "specialService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.ServiceB");
}
}

View File

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

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 34, "lineNumber" : 34,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "AUDIT_EVENT", "event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
@@ -19,7 +20,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 16, "lineNumber" : 16,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "FALLBACK_EVENT", "event" : "FALLBACK_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
@@ -29,7 +31,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "PROFILED_EVENT", "event" : "PROFILED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
@@ -39,7 +42,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "PRIMARY_EVENT", "event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
@@ -49,17 +53,19 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "EXTERNAL_TRIGGER", "event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit", "methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null, "sourceModule" : null,
"stateMachineId" : null, "stateMachineId" : "externalNotificationService",
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "SUBMIT_EVENT", "event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -69,7 +75,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 20, "lineNumber" : 20,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "CANCEL_EVENT", "event" : "CANCEL_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -79,7 +86,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 25, "lineNumber" : 25,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "[LIFECYCLE:RESTORE]", "event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -89,7 +97,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 29, "lineNumber" : 29,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "QUALIFIER_EVENT", "event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
@@ -99,7 +108,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "NAMED_EVENT", "event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
@@ -109,7 +119,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "AUTHORIZE", "event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl", "className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
@@ -119,7 +130,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 22, "lineNumber" : 22,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "CAPTURE", "event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl", "className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
@@ -129,7 +141,10 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 35, "lineNumber" : 35,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
}, { }, {
"event" : "[LIFECYCLE:RESTORE]", "event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl", "className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
@@ -139,7 +154,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 28, "lineNumber" : 28,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "REACTIVE_EVENT", "event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -149,7 +165,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 18, "lineNumber" : 18,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
} ], } ],
"entryPoints" : [ { "entryPoints" : [ {
"type" : "REST", "type" : "REST",
@@ -319,17 +336,14 @@
"methodName" : "processSubmit", "methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null, "sourceModule" : null,
"stateMachineId" : null, "stateMachineId" : "externalNotificationService",
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : null
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
} ]
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -353,7 +367,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 20, "lineNumber" : 20,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -384,7 +399,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 25, "lineNumber" : 25,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -415,7 +431,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 34, "lineNumber" : 34,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -450,7 +467,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 29, "lineNumber" : 29,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : "orderId", "contextMachineId" : "orderId",
"matchedTransitions" : null "matchedTransitions" : null
@@ -481,7 +499,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 18, "lineNumber" : 18,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -511,7 +530,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 16, "lineNumber" : 16,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -519,60 +539,6 @@
"targetState" : "START", "targetState" : "START",
"event" : "AUDIT_EVENT" "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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"contextMachineId" : null,
"matchedTransitions" : null
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -596,169 +562,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
}, "payloadTypes" : { }
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"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.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
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"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.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
},
"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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.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
},
"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.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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -785,88 +590,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -893,88 +618,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "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.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
},
"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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -1001,61 +646,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "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.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
},
"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.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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -1086,7 +678,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 22, "lineNumber" : 22,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -1117,7 +710,10 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 35, "lineNumber" : 35,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
}, },
"contextMachineId" : "paymentId", "contextMachineId" : "paymentId",
"matchedTransitions" : null "matchedTransitions" : null
@@ -1148,7 +744,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 28, "lineNumber" : 28,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : "paymentId", "contextMachineId" : "paymentId",
"matchedTransitions" : null "matchedTransitions" : null

View File

@@ -9,7 +9,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 34, "lineNumber" : 34,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "AUDIT_EVENT", "event" : "AUDIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor", "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
@@ -19,7 +20,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 16, "lineNumber" : 16,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "FALLBACK_EVENT", "event" : "FALLBACK_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
@@ -29,7 +31,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "PROFILED_EVENT", "event" : "PROFILED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
@@ -39,7 +42,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "PRIMARY_EVENT", "event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
@@ -49,17 +53,19 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "EXTERNAL_TRIGGER", "event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit", "methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null, "sourceModule" : null,
"stateMachineId" : null, "stateMachineId" : "externalNotificationService",
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "SUBMIT_EVENT", "event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -69,7 +75,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 20, "lineNumber" : 20,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "CANCEL_EVENT", "event" : "CANCEL_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -79,7 +86,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 25, "lineNumber" : 25,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "[LIFECYCLE:RESTORE]", "event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService", "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -89,7 +97,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 29, "lineNumber" : 29,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "QUALIFIER_EVENT", "event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
@@ -99,7 +108,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "NAMED_EVENT", "event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService", "className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
@@ -109,7 +119,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "AUTHORIZE", "event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl", "className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
@@ -119,7 +130,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 22, "lineNumber" : 22,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "CAPTURE", "event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl", "className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
@@ -129,7 +141,10 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 35, "lineNumber" : 35,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
}, { }, {
"event" : "[LIFECYCLE:RESTORE]", "event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl", "className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
@@ -139,7 +154,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 28, "lineNumber" : 28,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, { }, {
"event" : "REACTIVE_EVENT", "event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService", "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -149,7 +165,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 18, "lineNumber" : 18,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
} ], } ],
"entryPoints" : [ { "entryPoints" : [ {
"type" : "REST", "type" : "REST",
@@ -319,17 +336,14 @@
"methodName" : "processSubmit", "methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null, "sourceModule" : null,
"stateMachineId" : null, "stateMachineId" : "externalNotificationService",
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : null
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
} ]
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -353,7 +367,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 20, "lineNumber" : 20,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -384,7 +399,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 25, "lineNumber" : 25,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -415,7 +431,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 34, "lineNumber" : 34,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -450,7 +467,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 29, "lineNumber" : 29,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : "orderId", "contextMachineId" : "orderId",
"matchedTransitions" : null "matchedTransitions" : null
@@ -481,7 +499,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 18, "lineNumber" : 18,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -511,7 +530,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 16, "lineNumber" : 16,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : [ { "matchedTransitions" : [ {
@@ -519,60 +539,6 @@
"targetState" : "START", "targetState" : "START",
"event" : "AUDIT_EVENT" "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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"contextMachineId" : null,
"matchedTransitions" : null
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -596,169 +562,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "polymorphicEvents" : null,
}, "payloadTypes" : { }
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"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.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
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"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.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
},
"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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.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
},
"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.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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -785,88 +590,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -893,88 +618,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 17, "lineNumber" : 17,
"polymorphicEvents" : null "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.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
},
"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.FallbackQuirkService.doQuirk" ],
"triggerPoint" : {
"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
},
"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.ProfiledQuirkService.doQuirk" ],
"triggerPoint" : {
"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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -1001,61 +646,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 19, "lineNumber" : 19,
"polymorphicEvents" : null "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.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
},
"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.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
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -1086,7 +678,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 22, "lineNumber" : 22,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : null
@@ -1117,7 +710,10 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 35, "lineNumber" : 35,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : {
"paymentId" : "String"
}
}, },
"contextMachineId" : "paymentId", "contextMachineId" : "paymentId",
"matchedTransitions" : null "matchedTransitions" : null
@@ -1148,7 +744,8 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 28, "lineNumber" : 28,
"polymorphicEvents" : null "polymorphicEvents" : null,
"payloadTypes" : { }
}, },
"contextMachineId" : "paymentId", "contextMachineId" : "paymentId",
"matchedTransitions" : null "matchedTransitions" : null

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,6 @@
HELP.md
.gradle
build/ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/ out/
!**/src/main/**/out/ .gradle/
!**/src/test/**/out/ .idea/
*.class
### NetBeans ### *.log
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@@ -1,13 +0,0 @@
package click.kamil.examples.statemachine.extended.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MockBeanConfig {
@Bean(name = {"customMockBean", "aliasMockBean"})
public String myStringBean() {
return "Hello World";
}
}