Compare commits
1 Commits
working_bu
...
ff16c1bbe7
| Author | SHA1 | Date | |
|---|---|---|---|
| ff16c1bbe7 |
@@ -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,177 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
|
|
||||||
|
|
||||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||||
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName);
|
// First, check explicit variable targeting (like contextMachineId)
|
||||||
|
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();
|
||||||
|
// E.g., if target is "myStateMachine", ensure current machine name contains "my"
|
||||||
|
// If the variable name ends with StateMachine, we extract the prefix
|
||||||
|
if (targetVar.endsWith("statemachine")) {
|
||||||
|
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
|
||||||
|
if (!prefix.isEmpty() && !simplifiedMachineName.contains(prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 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 getDomainRootPackage(String[] p) {
|
||||||
|
if (p.length > 0 && (p[p.length - 1].equals("config") || p[p.length - 1].equals("configuration"))) {
|
||||||
|
return String.join(".", java.util.Arrays.copyOf(p, p.length - 1));
|
||||||
|
}
|
||||||
|
return String.join(".", p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeepSharedPackage(String pkg1, String pkg2) {
|
||||||
|
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null;
|
||||||
|
|
||||||
|
String domain1 = getDomainRootPackage(pkg1.split("\\."));
|
||||||
|
String[] p1 = domain1.split("\\.");
|
||||||
|
String[] p2 = pkg2.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require matching up to the domain root
|
||||||
|
int requiredMatchIdx = Math.max(1, p1.length - 1);
|
||||||
|
if (matchIdx >= requiredMatchIdx) {
|
||||||
|
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 domain1 = getDomainRootPackage(pkg1.split("\\."));
|
||||||
|
String[] p1 = domain1.split("\\.");
|
||||||
|
String[] p2 = pkg2.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic threshold based on domain root
|
||||||
|
int requiredBaseMatchIdx = p1.length - 2;
|
||||||
|
if (requiredBaseMatchIdx >= 1 && matchIdx == requiredBaseMatchIdx) {
|
||||||
|
return p2.length > matchIdx + 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String simplify(String name) {
|
private String simplify(String name) {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,75 +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 rawTriggerEvent = triggerPoint.getEvent();
|
|
||||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
|
||||||
String smEvent = simplify(smEventRaw);
|
|
||||||
|
|
||||||
boolean isWildcard = isWildcardVariable(rawTriggerEvent);
|
|
||||||
|
|
||||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
|
||||||
|
|
||||||
boolean hasPolyMatch = false;
|
|
||||||
for (String pe : polyEvents) {
|
|
||||||
if (pe.contains(".") && smEventRaw.contains(".")) {
|
|
||||||
if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe)) {
|
|
||||||
hasPolyMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
continue; // Stricter matching: do not fallback if both have qualifiers but don't match
|
|
||||||
}
|
|
||||||
|
|
||||||
String simplePe = pe;
|
|
||||||
if (pe.contains(".")) {
|
|
||||||
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
|
||||||
}
|
|
||||||
String simplifiedPe = simplify(simplePe);
|
|
||||||
|
|
||||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
|
||||||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
|
|
||||||
hasPolyMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasPolyMatch) return true;
|
|
||||||
if (polyEvents.isEmpty() && isWildcard) return true;
|
|
||||||
|
|
||||||
if (rawTriggerEvent.contains(".") && smEventRaw.contains(".")) {
|
|
||||||
if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String triggerEvent = simplify(rawTriggerEvent);
|
|
||||||
return smEvent.equals(triggerEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class BeanDefinition {
|
||||||
|
private String name;
|
||||||
|
private String typeFqn;
|
||||||
|
private List<String> qualifiers;
|
||||||
|
private boolean isPrimary;
|
||||||
|
private TypeDeclaration declaringClass;
|
||||||
|
private MethodDeclaration declaringMethod;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BeanRegistry {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final List<BeanDefinition> beans = new ArrayList<>();
|
||||||
|
|
||||||
|
public void register(BeanDefinition bean) {
|
||||||
|
beans.add(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BeanDefinition> getBeans() {
|
||||||
|
return beans;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BeanDefinition resolve(String expectedTypeFqn, String expectedName, String expectedQualifier) {
|
||||||
|
System.out.println("RESOLVING: type=" + expectedTypeFqn + " name=" + expectedName + " qual=" + expectedQualifier);
|
||||||
|
// 1. Qualifier Match
|
||||||
|
if (expectedQualifier != null && !expectedQualifier.isEmpty()) {
|
||||||
|
for (BeanDefinition bean : beans) {
|
||||||
|
if (matchesType(bean.getTypeFqn(), expectedTypeFqn) && bean.getQualifiers().contains(expectedQualifier)) {
|
||||||
|
System.out.println(" -> Matched by qualifier: " + bean.getName());
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BeanDefinition> typeMatches = new ArrayList<>();
|
||||||
|
for (BeanDefinition bean : beans) {
|
||||||
|
System.out.println(" Checking bean: " + bean.getName() + " of type " + bean.getTypeFqn());
|
||||||
|
if (matchesType(bean.getTypeFqn(), expectedTypeFqn)) {
|
||||||
|
typeMatches.add(bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeMatches.isEmpty()) {
|
||||||
|
System.out.println(" -> No type matches found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeMatches.size() == 1) {
|
||||||
|
System.out.println(" -> Single type match: " + typeMatches.get(0).getName());
|
||||||
|
return typeMatches.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Name Match
|
||||||
|
if (expectedName != null && !expectedName.isEmpty()) {
|
||||||
|
for (BeanDefinition bean : typeMatches) {
|
||||||
|
if (expectedName.equals(bean.getName())) {
|
||||||
|
System.out.println(" -> Matched by name: " + bean.getName());
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Primary Match
|
||||||
|
for (BeanDefinition bean : typeMatches) {
|
||||||
|
if (bean.isPrimary()) {
|
||||||
|
System.out.println(" -> Matched by primary: " + bean.getName());
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback
|
||||||
|
System.out.println(" -> Fallback (first match): " + typeMatches.get(0).getName());
|
||||||
|
return typeMatches.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesType(String actualTypeFqn, String expectedTypeFqn) {
|
||||||
|
if (actualTypeFqn == null || expectedTypeFqn == null) return false;
|
||||||
|
if (actualTypeFqn.equals(expectedTypeFqn)) return true;
|
||||||
|
|
||||||
|
// Check if actualTypeFqn implements expectedTypeFqn
|
||||||
|
List<String> actualImpls = context.getImplementations(expectedTypeFqn);
|
||||||
|
return actualImpls.contains(actualTypeFqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BeanScanner {
|
||||||
|
|
||||||
|
public static void scanAndRegister(CompilationUnit cu, CodebaseContext context, BeanRegistry registry) {
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(TypeDeclaration node) {
|
||||||
|
boolean isComponent = false;
|
||||||
|
boolean isPrimary = false;
|
||||||
|
String beanName = null;
|
||||||
|
List<String> qualifiers = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Object modifier : node.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
if (typeName.endsWith("Component") || typeName.endsWith("Service") ||
|
||||||
|
typeName.endsWith("Repository") || typeName.endsWith("Controller") ||
|
||||||
|
typeName.endsWith("Configuration")) {
|
||||||
|
isComponent = true;
|
||||||
|
String value = extractAnnotationValue(annotation, "value");
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
beanName = value.replace("\"", "");
|
||||||
|
}
|
||||||
|
} else if (typeName.endsWith("Primary")) {
|
||||||
|
isPrimary = true;
|
||||||
|
} else if (typeName.endsWith("Qualifier")) {
|
||||||
|
String value = extractAnnotationValue(annotation, "value");
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
qualifiers.add(value.replace("\"", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isComponent) {
|
||||||
|
if (beanName == null) {
|
||||||
|
beanName = decapitalize(node.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name(beanName)
|
||||||
|
.typeFqn(context.getFqn(node))
|
||||||
|
.qualifiers(qualifiers)
|
||||||
|
.isPrimary(isPrimary)
|
||||||
|
.declaringClass(node)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodDeclaration node) {
|
||||||
|
if (!(node.getParent() instanceof TypeDeclaration parentTd)) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isBean = false;
|
||||||
|
boolean isPrimary = false;
|
||||||
|
String beanName = null;
|
||||||
|
List<String> qualifiers = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Object modifier : node.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
if (typeName.endsWith("Bean")) {
|
||||||
|
isBean = true;
|
||||||
|
String value = extractAnnotationValue(annotation, "value");
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
value = extractAnnotationValue(annotation, "name");
|
||||||
|
}
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
beanName = value.replace("\"", "");
|
||||||
|
}
|
||||||
|
} else if (typeName.endsWith("Primary")) {
|
||||||
|
isPrimary = true;
|
||||||
|
} else if (typeName.endsWith("Qualifier")) {
|
||||||
|
String value = extractAnnotationValue(annotation, "value");
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
qualifiers.add(value.replace("\"", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBean && node.getReturnType2() != null) {
|
||||||
|
if (beanName == null) {
|
||||||
|
beanName = node.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
String returnType = extractTypeName(node.getReturnType2());
|
||||||
|
// We need the FQN of the return type. Try resolving it using context.
|
||||||
|
String returnTypeFqn = resolveFqn(returnType, parentTd, context);
|
||||||
|
if (returnTypeFqn == null) {
|
||||||
|
returnTypeFqn = returnType; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name(beanName)
|
||||||
|
.typeFqn(returnTypeFqn)
|
||||||
|
.qualifiers(qualifiers)
|
||||||
|
.isPrimary(isPrimary)
|
||||||
|
.declaringClass(parentTd)
|
||||||
|
.declaringMethod(node)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractAnnotationValue(Annotation annotation, String memberName) {
|
||||||
|
if (annotation instanceof SingleMemberAnnotation sma) {
|
||||||
|
if ("value".equals(memberName) || "name".equals(memberName)) { // @Bean("name") uses SingleMemberAnnotation with value()
|
||||||
|
return sma.getValue().toString();
|
||||||
|
}
|
||||||
|
} else if (annotation instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if (pair.getName().getIdentifier().equals(memberName)) {
|
||||||
|
return pair.getValue().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decapitalize(String name) {
|
||||||
|
if (name == null || name.isEmpty()) return name;
|
||||||
|
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
char[] chars = name.toCharArray();
|
||||||
|
chars[0] = Character.toLowerCase(chars[0]);
|
||||||
|
return new String(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractTypeName(Type type) {
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return extractTypeName(((ParameterizedType) type).getType());
|
||||||
|
} else if (type.isArrayType()) {
|
||||||
|
return extractTypeName(((ArrayType) type).getElementType());
|
||||||
|
}
|
||||||
|
return type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveFqn(String typeName, TypeDeclaration contextTd, CodebaseContext context) {
|
||||||
|
CompilationUnit cu = (CompilationUnit) contextTd.getRoot();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(typeName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
// Fallback to imports
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + typeName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,19 +17,6 @@ public class ConstantResolver {
|
|||||||
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
|
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
|
||||||
if (expr == null) return null;
|
if (expr == null) return null;
|
||||||
|
|
||||||
// Unwrap common wrappers
|
|
||||||
if (expr instanceof CastExpression ce) {
|
|
||||||
return resolveInternal(ce.getExpression(), context, visited);
|
|
||||||
}
|
|
||||||
if (expr instanceof ParenthesizedExpression pe) {
|
|
||||||
return resolveInternal(pe.getExpression(), context, visited);
|
|
||||||
}
|
|
||||||
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
|
|
||||||
if (!mi.arguments().isEmpty()) {
|
|
||||||
return resolveInternal((Expression) mi.arguments().get(0), context, visited);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Literal?
|
// 1. Literal?
|
||||||
if (expr instanceof StringLiteral sl) {
|
if (expr instanceof StringLiteral sl) {
|
||||||
return sl.getLiteralValue();
|
return sl.getLiteralValue();
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface PolymorphicEventResolver {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to resolve a dynamically passed event variable into a set of
|
|
||||||
* concrete polymorphic event names.
|
|
||||||
*
|
|
||||||
* @param triggerPoint The original trigger point to resolve.
|
|
||||||
* @param resolvedValue The current String expression (e.g., "event.getType()" or "event")
|
|
||||||
* @param path The call path trace leading up to this variable.
|
|
||||||
* @param context The codebase context for deep type resolution.
|
|
||||||
* @return The updated TriggerPoint with polymorphic events set, or the original if unable to resolve.
|
|
||||||
*/
|
|
||||||
TriggerPoint resolvePolymorphicEvents(TriggerPoint triggerPoint, String resolvedValue, List<String> path, CodebaseContext context);
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -172,97 +172,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()) {
|
||||||
@@ -325,10 +268,6 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expr instanceof ClassInstanceCreation cic) {
|
|
||||||
return cic.getType().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(expr instanceof MethodInvocation mi)) return null;
|
if (!(expr instanceof MethodInvocation mi)) return null;
|
||||||
|
|
||||||
// Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build()
|
// Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build()
|
||||||
|
|||||||
@@ -1,311 +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.*;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|
||||||
protected String currentMethodFqn;
|
|
||||||
protected Map<String, List<CallEdge>> graph;
|
|
||||||
|
|
||||||
public HeuristicCallGraphEngine(CodebaseContext context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
|
||||||
if (binding != null) {
|
|
||||||
return binding.getQualifiedName() + "." + methodName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiver instanceof ThisExpression) {
|
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
|
||||||
if (td != null) {
|
|
||||||
return context.getFqn(td) + "." + methodName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
|
||||||
if (fallbackTypeFqn != null) {
|
|
||||||
return fallbackTypeFqn + "." + methodName;
|
|
||||||
}
|
|
||||||
String receiverName = sn.getIdentifier();
|
|
||||||
return receiverName + "." + methodName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,360 +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 extends AbstractCallGraphEngine {
|
|
||||||
private final InjectionPointAnalyzer injectionAnalyzer;
|
|
||||||
private String currentMethodFqn;
|
|
||||||
private Map<String, List<CallEdge>> graph;
|
|
||||||
|
|
||||||
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
|
|
||||||
super(context);
|
|
||||||
this.injectionAnalyzer = injectionAnalyzer;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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) {
|
|
||||||
System.out.println("CALLGRAPH RESOLVING BEAN FOR BINDING: " + varBinding.getName() + " calling " + methodName);
|
|
||||||
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
|
||||||
if (concreteFqn != null) {
|
|
||||||
System.out.println(" -> RESOLVED CONCRETE FQN: " + concreteFqn);
|
|
||||||
return concreteFqn + "." + methodName;
|
|
||||||
} else {
|
|
||||||
System.out.println(" -> RESOLVE FAILED, RETURNED NULL");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.out.println("CALLGRAPH NAME BINDING IS NOT VARIABLE: " + nameBinding + " calling " + methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
|
||||||
if (binding != null) {
|
|
||||||
String typeName = null;
|
|
||||||
try {
|
|
||||||
if (binding.getErasure() != null) {
|
|
||||||
typeName = binding.getErasure().getQualifiedName();
|
|
||||||
if (typeName == null || typeName.isEmpty()) {
|
|
||||||
typeName = binding.getErasure().getName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Ignore JDT internal exceptions
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeName == null || typeName.isEmpty()) {
|
|
||||||
typeName = binding.getQualifiedName();
|
|
||||||
if (typeName == null || typeName.isEmpty()) {
|
|
||||||
typeName = binding.getName();
|
|
||||||
}
|
|
||||||
if (typeName != null && typeName.contains("<")) {
|
|
||||||
typeName = typeName.replaceAll("<.*>", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeName != null && !typeName.isEmpty()) {
|
|
||||||
return typeName + "." + 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,6 @@ 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;
|
||||||
@@ -25,7 +21,7 @@ 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;
|
||||||
@@ -36,17 +32,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
||||||
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);
|
||||||
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 +69,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
|
||||||
|
|||||||
@@ -1,93 +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()) {
|
|
||||||
boolean isAutowiredMethod = false;
|
|
||||||
for (IAnnotationBinding ann : method.getAnnotations()) {
|
|
||||||
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Autowired".equals(ann.getAnnotationType().getQualifiedName())) {
|
|
||||||
isAutowiredMethod = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method.isConstructor() || isAutowiredMethod) {
|
|
||||||
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())) {
|
|
||||||
// For setters, verify it roughly matches the field name or just rely on type.
|
|
||||||
// We will rely on type equality for now as a heuristic.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
public class SpringBean {
|
|
||||||
private String typeFqn;
|
|
||||||
|
|
||||||
@Builder.Default
|
|
||||||
private Set<String> assignableTypes = new HashSet<>();
|
|
||||||
|
|
||||||
@Builder.Default
|
|
||||||
private Set<String> beanNames = new HashSet<>();
|
|
||||||
|
|
||||||
@Builder.Default
|
|
||||||
private Set<String> qualifiers = new HashSet<>();
|
|
||||||
|
|
||||||
private boolean isPrimary;
|
|
||||||
private Integer order;
|
|
||||||
private String declaringClassFqn;
|
|
||||||
private String factoryMethodName;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class SpringBeanRegistry {
|
|
||||||
private final List<SpringBean> beans = new ArrayList<>();
|
|
||||||
|
|
||||||
public void addBean(SpringBean bean) {
|
|
||||||
beans.add(bean);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<SpringBean> getBeans() {
|
|
||||||
return Collections.unmodifiableList(beans);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,235 +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))
|
|
||||||
.order(extractOrder(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) {
|
|
||||||
Set<String> assignables = getAssignableTypes(returnType);
|
|
||||||
String[] concreteType = new String[] { returnType.getQualifiedName() };
|
|
||||||
|
|
||||||
if (node.getBody() != null) {
|
|
||||||
node.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement ret) {
|
|
||||||
if (ret.getExpression() != null) {
|
|
||||||
ITypeBinding exprType = ret.getExpression().resolveTypeBinding();
|
|
||||||
if (exprType != null && exprType.isClass()) {
|
|
||||||
concreteType[0] = exprType.getQualifiedName();
|
|
||||||
assignables.addAll(getAssignableTypes(exprType));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(ret);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
SpringBean bean = SpringBean.builder()
|
|
||||||
.typeFqn(concreteType[0])
|
|
||||||
.assignableTypes(assignables)
|
|
||||||
.isPrimary(hasAnnotation(methodBinding, "org.springframework.context.annotation.Primary"))
|
|
||||||
.qualifiers(extractQualifiers(methodBinding))
|
|
||||||
.order(extractOrder(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;
|
|
||||||
|
|
||||||
String qName = binding.getQualifiedName();
|
|
||||||
if (qName != null && !qName.isEmpty()) {
|
|
||||||
types.add(qName);
|
|
||||||
}
|
|
||||||
|
|
||||||
ITypeBinding erasure = binding.getErasure();
|
|
||||||
if (erasure != null) {
|
|
||||||
String erasureName = erasure.getQualifiedName();
|
|
||||||
if (erasureName != null && !erasureName.isEmpty()) {
|
|
||||||
types.add(erasureName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collectAssignableTypes(binding.getSuperclass(), types);
|
|
||||||
if (binding.getInterfaces() != null) {
|
|
||||||
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 Integer extractOrder(IBinding binding) {
|
|
||||||
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
|
||||||
if (ann.getAnnotationType() != null && "org.springframework.core.annotation.Order".equals(ann.getAnnotationType().getQualifiedName())) {
|
|
||||||
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
|
||||||
if ("value".equals(pair.getName())) {
|
|
||||||
Object val = pair.getValue();
|
|
||||||
if (val instanceof Integer) {
|
|
||||||
return (Integer) val;
|
|
||||||
} else if (val instanceof IVariableBinding) {
|
|
||||||
Object constantValue = ((IVariableBinding) val).getConstantValue();
|
|
||||||
if (constantValue instanceof Integer) {
|
|
||||||
return (Integer) constantValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Integer.MAX_VALUE; // Spring's default for @Order()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class SpringDependencyResolver {
|
|
||||||
private final SpringBeanRegistry registry;
|
|
||||||
|
|
||||||
public SpringDependencyResolver(SpringBeanRegistry registry) {
|
|
||||||
this.registry = registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 qualifier The value of the @Qualifier annotation on the injection point (if any).
|
|
||||||
* @param injectionName The name of the field or parameter being injected (used as fallback).
|
|
||||||
* @return A list of resolved beans. Usually 1. If 0, unresolved. If > 1, ambiguous.
|
|
||||||
*/
|
|
||||||
public List<SpringBean> resolve(String requiredTypeFqn, String qualifier, String injectionName) {
|
|
||||||
if (requiredTypeFqn == null) return new ArrayList<>();
|
|
||||||
|
|
||||||
// 1. Filter by Type (Exact FQN or Assignable Type)
|
|
||||||
System.out.println("RESOLVING " + requiredTypeFqn + " qual=" + qualifier + " injectionName=" + injectionName);
|
|
||||||
List<SpringBean> candidates = registry.getBeans().stream()
|
|
||||||
.filter(bean -> requiredTypeFqn.equals(bean.getTypeFqn()) || bean.getAssignableTypes().contains(requiredTypeFqn))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
System.out.println("CANDIDATES AFTER TYPE: " + candidates.stream().map(c -> c.getTypeFqn() + " (primary=" + c.isPrimary() + " names=" + c.getBeanNames() + " qual=" + c.getQualifiers() + ")").collect(Collectors.toList()));
|
|
||||||
if (candidates.isEmpty() || candidates.size() == 1) {
|
|
||||||
System.out.println("RETURNING: " + candidates.size());
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Filter by Qualifier (if provided)
|
|
||||||
if (qualifier != null && !qualifier.isEmpty()) {
|
|
||||||
List<SpringBean> qualifiedCandidates = candidates.stream()
|
|
||||||
.filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
if (qualifiedCandidates.size() == 1) {
|
|
||||||
return qualifiedCandidates;
|
|
||||||
} else if (!qualifiedCandidates.isEmpty()) {
|
|
||||||
candidates = qualifiedCandidates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Filter by @Primary
|
|
||||||
List<SpringBean> primaryCandidates = candidates.stream()
|
|
||||||
.filter(SpringBean::isPrimary)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
if (primaryCandidates.size() == 1) {
|
|
||||||
return primaryCandidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Fallback to Injection Name (field name or parameter name)
|
|
||||||
if (injectionName != null && !injectionName.isEmpty()) {
|
|
||||||
List<SpringBean> namedCandidates = candidates.stream()
|
|
||||||
.filter(bean -> bean.getBeanNames().contains(injectionName))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
if (namedCandidates.size() == 1) {
|
|
||||||
return namedCandidates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Fallback to @Order (lowest order wins)
|
|
||||||
Integer minOrder = candidates.stream()
|
|
||||||
.map(SpringBean::getOrder)
|
|
||||||
.filter(o -> o != null)
|
|
||||||
.min(Integer::compareTo)
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
if (minOrder != null) {
|
|
||||||
List<SpringBean> orderedCandidates = candidates.stream()
|
|
||||||
.filter(bean -> bean.getOrder() != null && bean.getOrder().equals(minOrder))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
if (!orderedCandidates.isEmpty()) {
|
|
||||||
candidates = orderedCandidates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return whatever candidates remain (could be ambiguous)
|
|
||||||
System.out.println("RETURNING: " + candidates.size());
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -292,15 +292,6 @@ public class AstTransitionParser {
|
|||||||
// Try same file
|
// Try same file
|
||||||
ASTNode declNode = cu.findDeclaringNode(binding.getKey());
|
ASTNode declNode = cu.findDeclaringNode(binding.getKey());
|
||||||
if (declNode instanceof MethodDeclaration md) {
|
if (declNode instanceof MethodDeclaration md) {
|
||||||
if (md.getBody() != null && md.getBody().statements().size() == 1) {
|
|
||||||
Object stmt = md.getBody().statements().get(0);
|
|
||||||
if (stmt instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
|
||||||
Expression retExpr = rs.getExpression();
|
|
||||||
if (isLambdaOrAnonymous(retExpr)) {
|
|
||||||
return retExpr.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return md.toString();
|
return md.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.eclipse.jdt.core.JavaCore;
|
import org.eclipse.jdt.core.JavaCore;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.BeanRegistry;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.BeanScanner;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -31,6 +33,7 @@ public class CodebaseContext {
|
|||||||
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||||
private final ConstantResolver constantResolver = new ConstantResolver();
|
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||||
private final PropertyResolver propertyResolver = new PropertyResolver();
|
private final PropertyResolver propertyResolver = new PropertyResolver();
|
||||||
|
private final BeanRegistry beanRegistry = new BeanRegistry(this);
|
||||||
private final List<String> activeProfiles = new ArrayList<>();
|
private final List<String> activeProfiles = new ArrayList<>();
|
||||||
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
||||||
private List<LibraryHint> libraryHints = new ArrayList<>();
|
private List<LibraryHint> libraryHints = new ArrayList<>();
|
||||||
@@ -167,6 +170,10 @@ public class CodebaseContext {
|
|||||||
scan(Set.of(rootDir), Collections.emptySet());
|
scan(Set.of(rootDir), Collections.emptySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BeanRegistry getBeanRegistry() {
|
||||||
|
return beanRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
private final List<CompilationUnit> allCus = new ArrayList<>();
|
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||||
|
|
||||||
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
||||||
@@ -194,6 +201,11 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post-processing: scan all parsed CompilationUnits for Spring beans
|
||||||
|
for (CompilationUnit cu : allCus) {
|
||||||
|
BeanScanner.scanAndRegister(cu, this, beanRegistry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
|
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
|
||||||
@@ -238,7 +250,6 @@ public class CodebaseContext {
|
|||||||
public List<String> getImplementations(String interfaceName) {
|
public List<String> getImplementations(String interfaceName) {
|
||||||
Set<String> allImpls = new HashSet<>();
|
Set<String> allImpls = new HashSet<>();
|
||||||
collectImplementations(interfaceName, allImpls, new HashSet<>());
|
collectImplementations(interfaceName, allImpls, new HashSet<>());
|
||||||
System.out.println("GET IMPLEMENTATIONS FOR: " + interfaceName + " -> " + allImpls);
|
|
||||||
return new ArrayList<>(allImpls);
|
return new ArrayList<>(allImpls);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,14 +627,31 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly
|
// If it's a leaf class (not abstract) and extends StateMachineConfigurerAdapter indirectly or has configurer methods
|
||||||
if (!isAbstract && extendsStateMachineConfigurerAdapter(td)) {
|
if (!isAbstract && (extendsStateMachineConfigurerAdapter(td) || hasConfigurerMethod(td))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasConfigurerMethod(TypeDeclaration td) {
|
||||||
|
for (MethodDeclaration method : td.getMethods()) {
|
||||||
|
if (method.parameters().isEmpty()) continue;
|
||||||
|
for (Object paramObj : method.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration param) {
|
||||||
|
String paramType = param.getType().toString();
|
||||||
|
if (paramType.contains("StateMachineTransitionConfigurer") ||
|
||||||
|
paramType.contains("StateMachineStateConfigurer") ||
|
||||||
|
paramType.contains("StateMachineConfigurationConfigurer")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
||||||
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -53,22 +53,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 +94,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");
|
||||||
|
|||||||
@@ -27,13 +27,18 @@ public class GoldenUpdater {
|
|||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
List<TestScenario> scenarios = provideTestScenarios();
|
List<TestScenario> scenarios = provideTestScenarios();
|
||||||
Path rootDir = Path.of("state_machine_exporter");
|
Path projectRoot = Path.of(System.getProperty("user.dir"));
|
||||||
|
if (projectRoot.endsWith("state_machine_exporter")) {
|
||||||
|
projectRoot = projectRoot.getParent();
|
||||||
|
}
|
||||||
|
Path rootDir = projectRoot.resolve("state_machine_exporter");
|
||||||
|
|
||||||
for (TestScenario scenario : scenarios) {
|
for (TestScenario scenario : scenarios) {
|
||||||
System.out.println("Updating golden files for: " + scenario.name());
|
System.out.println("Updating golden files for: " + scenario.name());
|
||||||
Path tempDir = Files.createTempDirectory("golden-update-");
|
Path tempDir = Files.createTempDirectory("golden-update-");
|
||||||
try {
|
try {
|
||||||
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true);
|
Path inputPath = projectRoot.resolve(scenario.inputPath());
|
||||||
|
exportService.runExporter(inputPath, tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles());
|
||||||
|
|
||||||
List<Path> generatedDirs;
|
List<Path> generatedDirs;
|
||||||
try (var stream = Files.list(tempDir)) {
|
try (var stream = Files.list(tempDir)) {
|
||||||
@@ -158,6 +163,49 @@ public class GoldenUpdater {
|
|||||||
Path.of("state_machines/inheritance_extra_functions3_state_machine"),
|
Path.of("state_machines/inheritance_extra_functions3_state_machine"),
|
||||||
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
||||||
"G2StateMachineConfiguration"
|
"G2StateMachineConfiguration"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Extended Analysis Sample",
|
||||||
|
Path.of("state_machines/extended_analysis_sample"),
|
||||||
|
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
|
||||||
|
"ExtendedStateMachineConfig"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Extended Analysis Sample (PROD)",
|
||||||
|
Path.of("state_machines/extended_analysis_sample"),
|
||||||
|
Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"),
|
||||||
|
"ExtendedStateMachineConfig",
|
||||||
|
List.of("prod")
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Inheritance Sample",
|
||||||
|
Path.of("state_machines/inheritance_sample"),
|
||||||
|
Path.of("src/test/resources/golden/InheritanceStateMachineConfig"),
|
||||||
|
"InheritanceStateMachineConfig"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Enterprise Order System",
|
||||||
|
Path.of("state_machines/enterprise_order_system"),
|
||||||
|
Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
|
||||||
|
"EnterpriseStateMachineConfig"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Multi-Module Sample",
|
||||||
|
Path.of("state_machines/multi_module_sample/core-module"),
|
||||||
|
Path.of("src/test/resources/golden/OrderStateMachineConfig"),
|
||||||
|
"OrderStateMachineConfig"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Maven Multi-Module Sample",
|
||||||
|
Path.of("state_machines/maven_multi_module/core-module"),
|
||||||
|
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
||||||
|
"MavenOrderStateMachine"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Complex Multi-Module Sample",
|
||||||
|
Path.of("state_machines/complex_multi_module_sm"),
|
||||||
|
Path.of("src/test/resources/golden/StateMachineConfig"),
|
||||||
|
"StateMachineConfig"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,12 +143,6 @@ public class RegressionTest {
|
|||||||
root.resolve("state_machines/complex_multi_module_sm"),
|
root.resolve("state_machines/complex_multi_module_sm"),
|
||||||
Path.of("src/test/resources/golden/StateMachineConfig"),
|
Path.of("src/test/resources/golden/StateMachineConfig"),
|
||||||
"StateMachineConfig"
|
"StateMachineConfig"
|
||||||
),
|
|
||||||
new TestScenario(
|
|
||||||
"Polymorphic Events Sample",
|
|
||||||
root.resolve("state_machines/polymorphic_events_sample"),
|
|
||||||
Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"),
|
|
||||||
"PolymorphicStateMachineConfiguration"
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -161,7 +155,7 @@ public class RegressionTest {
|
|||||||
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
|
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
|
||||||
if (tempDir == null) System.out.println("tempDir is NULL");
|
if (tempDir == null) System.out.println("tempDir is NULL");
|
||||||
|
|
||||||
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
|
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles());
|
||||||
|
|
||||||
// Find the generated directory (it might be named with FQN)
|
// Find the generated directory (it might be named with FQN)
|
||||||
List<Path> generatedDirs;
|
List<Path> generatedDirs;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class HeuristicEventMatchingEngineTest {
|
|
||||||
|
|
||||||
private final HeuristicEventMatchingEngine engine = new HeuristicEventMatchingEngine();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchExactFqn() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("com.example.OrderEvents.PAY").build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchEnumQualifierToFqn() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("OrderEvents.PAY").build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldPreventCrossEnumContamination() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("OrderEvents.PAY").build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldPreventCrossEnumContaminationWithPolymorphicEvents() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("OrderEvents.PAY")).build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchPolymorphicEnumQualifierToFqn() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("OrderEvents.PAY")).build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchRawStringToFqn() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("PAY").build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchRawStringToRawString() {
|
|
||||||
Event smEvent = Event.of("PAY", "PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("PAY").build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchPolymorphicRawStringToFqn() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("PAY")).build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class BeanRegistryTest {
|
||||||
|
|
||||||
|
private TestCodebaseContext context;
|
||||||
|
private BeanRegistry registry;
|
||||||
|
|
||||||
|
static class TestCodebaseContext extends CodebaseContext {
|
||||||
|
private final Map<String, List<String>> impls = new HashMap<>();
|
||||||
|
|
||||||
|
public void setImpls(String fqn, List<String> implList) {
|
||||||
|
impls.put(fqn, implList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getImplementations(String fqn) {
|
||||||
|
return impls.getOrDefault(fqn, List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
context = new TestCodebaseContext();
|
||||||
|
registry = new BeanRegistry(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolve_shouldMatchByExactType() {
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name("paymentService")
|
||||||
|
.typeFqn("com.example.PaymentService")
|
||||||
|
.qualifiers(List.of())
|
||||||
|
.isPrimary(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
BeanDefinition resolved = registry.resolve("com.example.PaymentService", null, null);
|
||||||
|
assertThat(resolved).isNotNull();
|
||||||
|
assertThat(resolved.getName()).isEqualTo("paymentService");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolve_shouldMatchByQualifierBypassingNameAndType() {
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name("emailNotifier")
|
||||||
|
.typeFqn("com.example.EmailNotifier")
|
||||||
|
.qualifiers(List.of("email"))
|
||||||
|
.isPrimary(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name("smsNotifier")
|
||||||
|
.typeFqn("com.example.SmsNotifier")
|
||||||
|
.qualifiers(List.of("sms"))
|
||||||
|
.isPrimary(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
context.setImpls("com.example.Notifier", List.of("com.example.EmailNotifier", "com.example.SmsNotifier"));
|
||||||
|
|
||||||
|
BeanDefinition resolved = registry.resolve("com.example.Notifier", "emailNotifier", "sms");
|
||||||
|
|
||||||
|
assertThat(resolved).isNotNull();
|
||||||
|
assertThat(resolved.getName()).isEqualTo("smsNotifier");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolve_shouldMatchByNameWhenNoQualifier() {
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name("fastProcessor")
|
||||||
|
.typeFqn("com.example.FastProcessor")
|
||||||
|
.qualifiers(List.of())
|
||||||
|
.isPrimary(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name("slowProcessor")
|
||||||
|
.typeFqn("com.example.SlowProcessor")
|
||||||
|
.qualifiers(List.of())
|
||||||
|
.isPrimary(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
context.setImpls("com.example.Processor", List.of("com.example.FastProcessor", "com.example.SlowProcessor"));
|
||||||
|
|
||||||
|
BeanDefinition resolved = registry.resolve("com.example.Processor", "fastProcessor", null);
|
||||||
|
|
||||||
|
assertThat(resolved).isNotNull();
|
||||||
|
assertThat(resolved.getName()).isEqualTo("fastProcessor");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolve_shouldMatchByPrimary() {
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name("localStorage")
|
||||||
|
.typeFqn("com.example.LocalStorage")
|
||||||
|
.qualifiers(List.of())
|
||||||
|
.isPrimary(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
registry.register(BeanDefinition.builder()
|
||||||
|
.name("cloudStorage")
|
||||||
|
.typeFqn("com.example.CloudStorage")
|
||||||
|
.qualifiers(List.of())
|
||||||
|
.isPrimary(true)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
context.setImpls("com.example.Storage", List.of("com.example.LocalStorage", "com.example.CloudStorage"));
|
||||||
|
|
||||||
|
// Name doesn't match either, no qualifier
|
||||||
|
BeanDefinition resolved = registry.resolve("com.example.Storage", "storage", null);
|
||||||
|
|
||||||
|
assertThat(resolved).isNotNull();
|
||||||
|
assertThat(resolved.getName()).isEqualTo("cloudStorage");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,69 +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.ast.common.CodebaseContext;
|
|
||||||
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.List;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class BuilderLocalVariableTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldTraceLocalVariableInBuilderPattern(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent() {
|
|
||||||
OrderEvents e = OrderEvents.PAY;
|
|
||||||
RichOrderEvent event = new RichOrderEvent();
|
|
||||||
event.setType(e);
|
|
||||||
service.updateOrderState(event.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
public void setType(OrderEvents type) { this.type = type; }
|
|
||||||
public OrderEvents getType() { return type; }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
System.out.println("POLY EVENTS FOUND: " + chain.getTriggerPoint().getPolymorphicEvents());
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,190 +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
|
|
||||||
void shouldResolveLocalSetterAndSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderService {
|
|
||||||
public void updateOrderState(String vv) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
MysteryPayload a = new MysteryPayload();
|
|
||||||
a.setType(my_func(vv));
|
|
||||||
sm.sendEvent(a.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
private OrderEvents my_func(String q) {
|
|
||||||
return switch (q) {
|
|
||||||
case "a" -> OrderEvents.A8;
|
|
||||||
case "b" -> OrderEvents.A133;
|
|
||||||
default -> throw new IllegalArgumentException("Unknown");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { A8, A133 }
|
|
||||||
|
|
||||||
class MysteryPayload {
|
|
||||||
private OrderEvents type;
|
|
||||||
public void setType(OrderEvents type) { this.type = type; }
|
|
||||||
public OrderEvents getType() { return type; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateMachine {
|
|
||||||
public void sendEvent(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.StateMachine")
|
|
||||||
.methodName("sendEvent")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
|
|
||||||
System.out.println("Resolved poly events: " + chain.getTriggerPoint().getPolymorphicEvents());
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveOldStyleSwitchStatementPolymorphism(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderService {
|
|
||||||
public void updateOrderState(String vv) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(getEventFromOldSwitch(vv));
|
|
||||||
}
|
|
||||||
|
|
||||||
private OrderEvents getEventFromOldSwitch(String q) {
|
|
||||||
switch (q) {
|
|
||||||
case "a": return OrderEvents.A8;
|
|
||||||
case "b":
|
|
||||||
case "c": return OrderEvents.A133;
|
|
||||||
default: return OrderEvents.PAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { A8, A133, PAY }
|
|
||||||
|
|
||||||
class StateMachine {
|
|
||||||
public void sendEvent(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveMultiValueSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderService {
|
|
||||||
public void updateOrderState(String vv) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(getEventMultiValue(vv));
|
|
||||||
}
|
|
||||||
|
|
||||||
private OrderEvents getEventMultiValue(String q) {
|
|
||||||
return switch (q) {
|
|
||||||
case "a", "b" -> OrderEvents.A8;
|
|
||||||
case "c" -> OrderEvents.A133;
|
|
||||||
default -> OrderEvents.PAY;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { A8, A133, PAY }
|
|
||||||
|
|
||||||
class StateMachine {
|
|
||||||
public void sendEvent(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveSwitchExpressionWithBlockAndYieldPolymorphism(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderService {
|
|
||||||
public void updateOrderState(String vv) {
|
|
||||||
StateMachine sm = new StateMachine();
|
|
||||||
sm.sendEvent(getEventWithYield(vv));
|
|
||||||
}
|
|
||||||
|
|
||||||
private OrderEvents getEventWithYield(String q) {
|
|
||||||
return switch (q) {
|
|
||||||
case "a" -> {
|
|
||||||
System.out.println("Processing a");
|
|
||||||
yield OrderEvents.A8;
|
|
||||||
}
|
|
||||||
case "b" -> { yield OrderEvents.A133; }
|
|
||||||
default -> OrderEvents.PAY;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { A8, A133, PAY }
|
|
||||||
|
|
||||||
class StateMachine {
|
|
||||||
public void sendEvent(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException {
|
void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException {
|
||||||
@@ -224,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")
|
||||||
@@ -275,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")
|
||||||
@@ -327,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")
|
||||||
@@ -381,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")
|
||||||
@@ -449,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")
|
||||||
@@ -471,63 +290,6 @@ class HeuristicCallGraphEngineTest {
|
|||||||
.containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED");
|
.containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveTernaryAndStringPolymorphicReturns() throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent() {
|
|
||||||
service.updateOrderState(new MysteryPayload().getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(Object event) {
|
|
||||||
// sendEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MysteryPayload {
|
|
||||||
public Object getType() {
|
|
||||||
int a = 5;
|
|
||||||
if (a > 10) {
|
|
||||||
return "RAW_STRING_EVENT";
|
|
||||||
}
|
|
||||||
return a > 5 ? OrderEvents.ABCD : OrderEvents.PAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { ABCD, PAY }
|
|
||||||
""";
|
|
||||||
Path tempDir = Files.createTempDirectory("callgraph_test_ternary_string");
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.ABCD", "OrderEvents.PAY", "RAW_STRING_EVENT");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUnwrapDeepMethodWrappers() throws IOException {
|
void shouldUnwrapDeepMethodWrappers() throws IOException {
|
||||||
String source = """
|
String source = """
|
||||||
@@ -560,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")
|
||||||
@@ -630,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")
|
||||||
@@ -676,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")
|
||||||
@@ -727,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")
|
||||||
@@ -785,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")
|
||||||
@@ -835,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")
|
||||||
@@ -888,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")
|
||||||
@@ -933,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")
|
||||||
@@ -985,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")
|
||||||
@@ -1031,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")
|
||||||
@@ -1075,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")
|
||||||
@@ -1125,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")
|
||||||
@@ -1171,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")
|
||||||
@@ -1222,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();
|
||||||
@@ -1255,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();
|
||||||
@@ -1311,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();
|
||||||
@@ -1320,7 +1082,6 @@ class HeuristicCallGraphEngineTest {
|
|||||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B");
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
|
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
|
||||||
String source = """
|
String source = """
|
||||||
@@ -1353,7 +1114,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();
|
||||||
@@ -1387,7 +1148,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();
|
||||||
@@ -1419,7 +1180,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();
|
||||||
@@ -1456,7 +1217,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();
|
||||||
@@ -1493,7 +1254,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();
|
||||||
@@ -1528,7 +1289,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();
|
||||||
@@ -1565,7 +1326,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();
|
||||||
@@ -1599,7 +1360,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();
|
||||||
@@ -1635,7 +1396,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();
|
||||||
@@ -1669,7 +1430,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();
|
||||||
@@ -1707,7 +1468,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();
|
||||||
@@ -1748,7 +1509,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();
|
||||||
@@ -1786,7 +1547,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();
|
||||||
@@ -1825,7 +1586,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();
|
||||||
@@ -1868,7 +1629,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();
|
||||||
@@ -1910,7 +1671,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();
|
||||||
@@ -1951,7 +1712,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();
|
||||||
@@ -1992,7 +1753,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();
|
||||||
@@ -2035,7 +1796,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();
|
||||||
@@ -2080,7 +1841,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();
|
||||||
@@ -2125,7 +1886,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();
|
||||||
@@ -2135,4 +1896,73 @@ class HeuristicCallGraphEngineTest {
|
|||||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNarrowPolymorphicCallUsingBeanResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
|
||||||
|
public interface Notifier {
|
||||||
|
void send(String event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component("emailNotifier")
|
||||||
|
class EmailNotifier implements Notifier {
|
||||||
|
public void send(String event) {
|
||||||
|
// Email implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component("smsNotifier")
|
||||||
|
class SmsNotifier implements Notifier {
|
||||||
|
public void send(String event) {
|
||||||
|
// SMS implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class OrderService {
|
||||||
|
private final Notifier smsNotifier;
|
||||||
|
|
||||||
|
public OrderService(Notifier smsNotifier) {
|
||||||
|
this.smsNotifier = smsNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processOrder() {
|
||||||
|
smsNotifier.send("ORDER_COMPLETED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
CallGraphBuilder builder = new CallGraphBuilder(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("processOrder")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint triggerEmail = TriggerPoint.builder()
|
||||||
|
.className("com.example.EmailNotifier")
|
||||||
|
.methodName("send")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint triggerSms = TriggerPoint.builder()
|
||||||
|
.className("com.example.SmsNotifier")
|
||||||
|
.methodName("send")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(triggerEmail, triggerSms));
|
||||||
|
|
||||||
|
// Because 'smsNotifier' perfectly matches the component name "smsNotifier",
|
||||||
|
// the bean resolution should narrow the call graph to ONLY SmsNotifier.
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getClassName()).isEqualTo("com.example.SmsNotifier");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,412 +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.ast.common.CodebaseContext;
|
|
||||||
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.List;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class ConstructorInvocationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotBlindlyAddAllConstructorArguments(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
private String irrelevantInfo;
|
|
||||||
|
|
||||||
public RichOrderEvent() {
|
|
||||||
this(OrderEvents.PAY, "DEFAULT_INFO");
|
|
||||||
}
|
|
||||||
|
|
||||||
public RichOrderEvent(OrderEvents type, String info) {
|
|
||||||
this.type = type;
|
|
||||||
this.irrelevantInfo = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEvents getType() { return type; }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY")
|
|
||||||
.doesNotContain("DEFAULT_INFO");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleOverloadedConstructorsGracefully(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
private String irrelevantInfo;
|
|
||||||
private int count;
|
|
||||||
|
|
||||||
public RichOrderEvent() {
|
|
||||||
this(OrderEvents.PAY, "DEFAULT_INFO");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overloaded constructor 1
|
|
||||||
public RichOrderEvent(OrderEvents type, String info) {
|
|
||||||
this.type = type;
|
|
||||||
this.irrelevantInfo = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overloaded constructor 2 (same number of arguments!)
|
|
||||||
public RichOrderEvent(String info, int count) {
|
|
||||||
this.irrelevantInfo = info;
|
|
||||||
this.count = count;
|
|
||||||
// type is not set here
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEvents getType() { return type; }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY")
|
|
||||||
.doesNotContain("DEFAULT_INFO")
|
|
||||||
.doesNotContain("count");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleChainedConstructorDelegationGracefully(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BaseEvent {
|
|
||||||
protected OrderEvents type;
|
|
||||||
public BaseEvent(OrderEvents type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent extends BaseEvent {
|
|
||||||
public RichOrderEvent() {
|
|
||||||
this(OrderEvents.PAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RichOrderEvent(OrderEvents type) {
|
|
||||||
this(type, "DEFAULT_INFO");
|
|
||||||
}
|
|
||||||
|
|
||||||
public RichOrderEvent(OrderEvents type, String info) {
|
|
||||||
super(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEvents getType() { return type; }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY")
|
|
||||||
.doesNotContain("DEFAULT_INFO");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleFieldAccessGracefully(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
|
|
||||||
public RichOrderEvent() {
|
|
||||||
this(OrderEvents.PAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RichOrderEvent(OrderEvents type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEvents getType() {
|
|
||||||
return this.type; // Specifically testing FieldAccess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
void shouldTraceThroughWrappedConstructorArguments(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
|
|
||||||
public RichOrderEvent() {
|
|
||||||
this(Objects.requireNonNull(OrderEvents.PAY));
|
|
||||||
}
|
|
||||||
|
|
||||||
public RichOrderEvent(OrderEvents type) {
|
|
||||||
this((OrderEvents) type, "DEFAULT_INFO");
|
|
||||||
}
|
|
||||||
|
|
||||||
public RichOrderEvent(OrderEvents type, String info) {
|
|
||||||
this.type = Objects.requireNonNull(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEvents getType() { return type; }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldTraceThroughComplexOverloadedConstructors(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(EnterpriseEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EnterpriseEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
private String info;
|
|
||||||
|
|
||||||
public EnterpriseEvent() {
|
|
||||||
this(OrderEvents.PAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches by parameter count but wrong type!
|
|
||||||
public EnterpriseEvent(String info) {
|
|
||||||
this.info = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EnterpriseEvent(OrderEvents type) {
|
|
||||||
this("info", type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target of previous constructor
|
|
||||||
public EnterpriseEvent(String info, OrderEvents type) {
|
|
||||||
this(type, info, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final destination
|
|
||||||
public EnterpriseEvent(OrderEvents type, String info, Object extra) {
|
|
||||||
this.type = type;
|
|
||||||
this.info = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEvents getType() { return type; }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,72 +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.ast.common.CodebaseContext;
|
|
||||||
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.List;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class ConstructorLocalVariableTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldTraceLocalVariableInConstructor(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
|
|
||||||
public RichOrderEvent() {
|
|
||||||
OrderEvents e = OrderEvents.PAY;
|
|
||||||
this.type = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrderEvents getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,285 +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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
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.PrimaryQuirkService")
|
|
||||||
.methodName("doQuirk")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getMethodChain())
|
|
||||||
.containsExactly(
|
|
||||||
"click.kamil.examples.statemachine.extended.web.QuirkController.testFallback",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveBeanParameterInjection() {
|
|
||||||
EntryPoint ep = EntryPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.config.BeanParameterTestConfig")
|
|
||||||
.methodName("myBeanParamTester")
|
|
||||||
.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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getMethodChain())
|
|
||||||
.containsExactly(
|
|
||||||
"click.kamil.examples.statemachine.extended.config.BeanParameterTestConfig.myBeanParamTester",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveOrderedBeanInjection() {
|
|
||||||
EntryPoint ep = EntryPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.web.OrderedTestController")
|
|
||||||
.methodName("testOrder")
|
|
||||||
.build();
|
|
||||||
TriggerPoint tp = TriggerPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.service.HighPriorityOrderedService")
|
|
||||||
.methodName("doAction")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getMethodChain())
|
|
||||||
.containsExactly(
|
|
||||||
"click.kamil.examples.statemachine.extended.web.OrderedTestController.testOrder",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.HighPriorityOrderedService.doAction"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveFieldInjection() {
|
|
||||||
EntryPoint ep = EntryPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.web.FieldInjectionController")
|
|
||||||
.methodName("testField")
|
|
||||||
.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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getMethodChain()).contains(
|
|
||||||
"click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveSetterInjection() {
|
|
||||||
EntryPoint ep = EntryPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.web.SetterInjectionController")
|
|
||||||
.methodName("testSetter")
|
|
||||||
.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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getMethodChain()).contains(
|
|
||||||
"click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveAbstractInheritedBean() {
|
|
||||||
EntryPoint ep = EntryPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.config.AbstractBaseConfig")
|
|
||||||
.methodName("inheritedBeanTester")
|
|
||||||
.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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getMethodChain()).contains(
|
|
||||||
"click.kamil.examples.statemachine.extended.config.AbstractBaseConfig.inheritedBeanTester",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveAmbiguousListFallback() {
|
|
||||||
EntryPoint ep = EntryPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.web.ListInjectionController")
|
|
||||||
.methodName("testList")
|
|
||||||
.build();
|
|
||||||
TriggerPoint tp1 = TriggerPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.service.AmbiguousA")
|
|
||||||
.methodName("doAmbig")
|
|
||||||
.build();
|
|
||||||
TriggerPoint tp2 = TriggerPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.service.AmbiguousB")
|
|
||||||
.methodName("doAmbig")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains1 = engine.findChains(List.of(ep), List.of(tp1));
|
|
||||||
List<CallChain> chains2 = engine.findChains(List.of(ep), List.of(tp2));
|
|
||||||
|
|
||||||
assertThat(chains1).isNotEmpty();
|
|
||||||
assertThat(chains1.get(0).getMethodChain()).contains(
|
|
||||||
"click.kamil.examples.statemachine.extended.web.ListInjectionController.testList",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.AmbiguousA.doAmbig"
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(chains2).isNotEmpty();
|
|
||||||
assertThat(chains2.get(0).getMethodChain()).contains(
|
|
||||||
"click.kamil.examples.statemachine.extended.web.ListInjectionController.testList",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.AmbiguousB.doAmbig"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveConcreteReturnType() {
|
|
||||||
EntryPoint ep = EntryPoint.builder()
|
|
||||||
.className("click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController")
|
|
||||||
.methodName("testConcrete")
|
|
||||||
.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));
|
|
||||||
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
|
|
||||||
assertThat(chains.get(0).getMethodChain()).contains(
|
|
||||||
"click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete",
|
|
||||||
"click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +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.ast.common.CodebaseContext;
|
|
||||||
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.List;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class LocalVariableTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldTraceLocalVariableInGetter(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
public OrderEvents getType() {
|
|
||||||
OrderEvents e = OrderEvents.PAY;
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY")
|
|
||||||
.doesNotContain("e"); // Should not blindly add the variable name
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldTraceMultipleAssignments(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
public OrderEvents getType() {
|
|
||||||
OrderEvents e = null;
|
|
||||||
if (System.currentTimeMillis() % 2 == 0) {
|
|
||||||
e = OrderEvents.PAY;
|
|
||||||
} else {
|
|
||||||
e = OrderEvents.CANCEL;
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY, CANCEL }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCEL");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +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.ast.common.CodebaseContext;
|
|
||||||
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.List;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class ReturnStatementTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleAssignmentInReturn(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RichOrderEvent {
|
|
||||||
private OrderEvents type;
|
|
||||||
public OrderEvents getType() {
|
|
||||||
return this.type = OrderEvents.PAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.containsExactlyInAnyOrder("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +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.ast.common.CodebaseContext;
|
|
||||||
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.List;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
class SuperMethodInvocationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleSuperMethodInvocation(@TempDir Path tempDir) throws IOException {
|
|
||||||
String source = """
|
|
||||||
package com.example;
|
|
||||||
public class OrderController {
|
|
||||||
private OrderService service;
|
|
||||||
public void processOrderEvent(DerivedEvent domainEvent) {
|
|
||||||
service.updateOrderState(domainEvent.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderService {
|
|
||||||
public void updateOrderState(OrderEvents event) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BaseEvent {
|
|
||||||
public OrderEvents getType() {
|
|
||||||
return OrderEvents.PAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DerivedEvent extends BaseEvent {
|
|
||||||
public OrderEvents getType() {
|
|
||||||
return super.getType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderEvents { PAY }
|
|
||||||
""";
|
|
||||||
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
|
||||||
|
|
||||||
CodebaseContext context = new CodebaseContext();
|
|
||||||
context.scan(tempDir);
|
|
||||||
|
|
||||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
|
||||||
|
|
||||||
EntryPoint entryPoint = EntryPoint.builder()
|
|
||||||
.className("com.example.OrderController")
|
|
||||||
.methodName("processOrderEvent")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
|
||||||
.className("com.example.OrderService")
|
|
||||||
.methodName("updateOrderState")
|
|
||||||
.event("event")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
|
||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
|
||||||
CallChain chain = chains.get(0);
|
|
||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
|
||||||
.contains("OrderEvents.PAY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +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");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveByOrderWhenOtherFiltersTie() {
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.LowestOrderService")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(Integer.MAX_VALUE) // Lowest precedence
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.HighestOrderService")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(Integer.MIN_VALUE) // Highest precedence
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.MiddleOrderService")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(0)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
|
|
||||||
|
|
||||||
assertThat(resolved).hasSize(1);
|
|
||||||
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.HighestOrderService");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveByOrderWithCustomValues() {
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.ServiceA")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(400)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.ServiceB")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(100)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.ServiceC")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(2147483647 + 400) // This wraps around in Java due to overflow, but let's test a very large valid value
|
|
||||||
.build());
|
|
||||||
|
|
||||||
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
|
|
||||||
|
|
||||||
assertThat(resolved).hasSize(1);
|
|
||||||
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.ServiceC");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void primaryShouldOverrideOrder() {
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.PrimaryButLowestOrder")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.isPrimary(true)
|
|
||||||
.order(Integer.MAX_VALUE)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.NotPrimaryButHighestOrder")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.isPrimary(false)
|
|
||||||
.order(Integer.MIN_VALUE)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
|
|
||||||
|
|
||||||
assertThat(resolved).hasSize(1);
|
|
||||||
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.PrimaryButLowestOrder");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void qualifierShouldOverridePrimaryAndOrder() {
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.PrimaryService")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.isPrimary(true)
|
|
||||||
.order(Integer.MIN_VALUE)
|
|
||||||
.qualifiers(Set.of("primary"))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.QualifiedService")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.isPrimary(false)
|
|
||||||
.order(Integer.MAX_VALUE)
|
|
||||||
.qualifiers(Set.of("targetQualifier"))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", "targetQualifier", "someUnknownName");
|
|
||||||
|
|
||||||
assertThat(resolved).hasSize(1);
|
|
||||||
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.QualifiedService");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldReturnMultipleWhenOrderIsTied() {
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.ServiceA")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(10)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.ServiceB")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(10)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
registry.addBean(SpringBean.builder()
|
|
||||||
.typeFqn("com.example.ServiceC")
|
|
||||||
.assignableTypes(Set.of("com.example.PaymentService"))
|
|
||||||
.order(20)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
|
|
||||||
|
|
||||||
assertThat(resolved).hasSize(2);
|
|
||||||
assertThat(resolved.stream().map(SpringBean::getTypeFqn))
|
|
||||||
.containsExactlyInAnyOrder("com.example.ServiceA", "com.example.ServiceB");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -261,56 +261,6 @@ class AstTransitionParserTest {
|
|||||||
.contains("System.out.println(\"Hello\")");
|
.contains("System.out.println(\"Hello\")");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldExtractInternalLogicForSameFileMethodReturningLambda() {
|
|
||||||
String source = """
|
|
||||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
|
||||||
import org.springframework.statemachine.guard.Guard;
|
|
||||||
public class TestClass {
|
|
||||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
|
||||||
transitions
|
|
||||||
.withExternal()
|
|
||||||
.source("S1")
|
|
||||||
.target("S2")
|
|
||||||
.guard(guardVarEquals("test"));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Guard<String, String> guardVarEquals(String expected) {
|
|
||||||
return context -> {
|
|
||||||
return expected.equals(context.getMessage());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
MethodDeclaration method = createMethodDeclarationWithBindings(source);
|
|
||||||
|
|
||||||
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
|
|
||||||
|
|
||||||
assertThat(transitions).hasSize(1);
|
|
||||||
assertThat(transitions.getFirst().getGuard().internalLogic())
|
|
||||||
.contains("return expected.equals(context.getMessage());")
|
|
||||||
.doesNotContain("protected Guard<String, String> guardVarEquals");
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration createMethodDeclarationWithBindings(String source) {
|
|
||||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
|
||||||
parser.setResolveBindings(true);
|
|
||||||
parser.setBindingsRecovery(true);
|
|
||||||
parser.setEnvironment(new String[0], new String[0], null, true);
|
|
||||||
parser.setUnitName("TestClass.java");
|
|
||||||
parser.setSource(source.toCharArray());
|
|
||||||
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
|
|
||||||
if (cu.types().isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().getFirst();
|
|
||||||
MethodDeclaration[] methods = typeDecl.getMethods();
|
|
||||||
if (methods.length > 0) {
|
|
||||||
return methods[0];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
private MethodDeclaration createMethodDeclaration(String source) {
|
private MethodDeclaration createMethodDeclaration(String source) {
|
||||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
@@ -1,15 +0,0 @@
|
|||||||
digraph statemachine {
|
|
||||||
rankdir=LR;
|
|
||||||
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
|
||||||
edge [fontname="Arial", fontsize=10];
|
|
||||||
|
|
||||||
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
|
||||||
_start -> SUBMITTED;
|
|
||||||
CANCELED [fillcolor=lightgray];
|
|
||||||
FULFILLED [fillcolor=lightgray];
|
|
||||||
SUBMITTED -> PAID [label="OrderEvents.PAY", style="solid", color="black"];
|
|
||||||
PAID -> FULFILLED [label="OrderEvents.FULFILL", style="solid", color="black"];
|
|
||||||
SUBMITTED -> CANCELED [label="OrderEvents.CANCEL", style="solid", color="black"];
|
|
||||||
PAID -> CANCELED [label="OrderEvents.ABCD", style="solid", color="black"];
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,631 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata" : {
|
|
||||||
"triggers" : [ {
|
|
||||||
"event" : "event",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
}, {
|
|
||||||
"event" : "payload",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processPayloadEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
}, {
|
|
||||||
"event" : "event",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processCustomEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 21,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
} ],
|
|
||||||
"entryPoints" : [ {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "pay",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /fulfill",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "fulfill",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/fulfill",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /cancel",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "cancel",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/cancel",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /payload-pay",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payloadPay",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/payload-pay",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-variable",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payVariable",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-variable",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-cast",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payCast",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-cast",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-ternary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payTernary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-ternary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ {
|
|
||||||
"name" : "isPay",
|
|
||||||
"type" : "boolean",
|
|
||||||
"annotations" : [ ]
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-list",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payList",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-list",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-builder-static",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payBuilderStatic",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-builder-static",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-builder-instance",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payBuilderInstance",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-builder-instance",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /abcd",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "abcd",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/abcd",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
}, {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /mystery",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "mystery",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/mystery",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
} ],
|
|
||||||
"callChains" : [ {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "pay",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.pay", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new PayEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.PAID",
|
|
||||||
"event" : "OrderEvents.PAY"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /fulfill",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "fulfill",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/fulfill",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.fulfill", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new FulfillEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.FULFILL" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.PAID",
|
|
||||||
"targetState" : "OrderStates.FULFILLED",
|
|
||||||
"event" : "OrderEvents.FULFILL"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /cancel",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "cancel",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/cancel",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.cancel", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new CancelEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.CANCEL" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.CANCELED",
|
|
||||||
"event" : "OrderEvents.CANCEL"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /payload-pay",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payloadPay",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/payload-pay",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payloadPay", "click.kamil.examples.statemachine.polymorphic.OrderService.processPayloadEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new PayEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processPayloadEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.PAID",
|
|
||||||
"event" : "OrderEvents.PAY"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-variable",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payVariable",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-variable",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payVariable", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new PayEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.PAID",
|
|
||||||
"event" : "OrderEvents.PAY"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-cast",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payCast",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-cast",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payCast", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "(BaseEvent)new PayEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.PAID",
|
|
||||||
"event" : "OrderEvents.PAY"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-ternary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payTernary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-ternary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ {
|
|
||||||
"name" : "isPay",
|
|
||||||
"type" : "boolean",
|
|
||||||
"annotations" : [ ]
|
|
||||||
} ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payTernary", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "isPay ? new PayEvent() : new CancelEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-list",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payList",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-list",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "event",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.PAID",
|
|
||||||
"event" : "OrderEvents.PAY"
|
|
||||||
}, {
|
|
||||||
"sourceState" : "OrderStates.PAID",
|
|
||||||
"targetState" : "OrderStates.FULFILLED",
|
|
||||||
"event" : "OrderEvents.FULFILL"
|
|
||||||
}, {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.CANCELED",
|
|
||||||
"event" : "OrderEvents.CANCEL"
|
|
||||||
}, {
|
|
||||||
"sourceState" : "OrderStates.PAID",
|
|
||||||
"targetState" : "OrderStates.CANCELED",
|
|
||||||
"event" : "OrderEvents.ABCD"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-builder-static",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payBuilderStatic",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-builder-static",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "EventBuilder.buildEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /pay-builder-instance",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "payBuilderInstance",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/pay-builder-instance",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new EventBuilder().buildInstanceEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 13,
|
|
||||||
"polymorphicEvents" : [ ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /abcd",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "abcd",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/abcd",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.abcd", "click.kamil.examples.statemachine.polymorphic.OrderService.processCustomEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new AbcdEvent()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processCustomEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 21,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.ABCD" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.PAID",
|
|
||||||
"targetState" : "OrderStates.CANCELED",
|
|
||||||
"event" : "OrderEvents.ABCD"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /mystery",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
|
|
||||||
"methodName" : "mystery",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/mystery",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.mystery", "click.kamil.examples.statemachine.polymorphic.OrderService.processCustomEvent" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "new MysteryPayload()",
|
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
|
||||||
"methodName" : "processCustomEvent",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 21,
|
|
||||||
"polymorphicEvents" : [ "OrderEvents.ABCD", "OrderEvents.PAY" ]
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.PAID",
|
|
||||||
"event" : "OrderEvents.PAY"
|
|
||||||
}, {
|
|
||||||
"sourceState" : "OrderStates.PAID",
|
|
||||||
"targetState" : "OrderStates.CANCELED",
|
|
||||||
"event" : "OrderEvents.ABCD"
|
|
||||||
} ]
|
|
||||||
} ],
|
|
||||||
"properties" : {
|
|
||||||
"default" : {
|
|
||||||
"spring.application.name" : "statemachinedemo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name" : "click.kamil.examples.statemachine.polymorphic.PolymorphicStateMachineConfiguration",
|
|
||||||
"renderChoicesAsDiamonds" : true,
|
|
||||||
"startStates" : [ "OrderStates.SUBMITTED" ],
|
|
||||||
"transitions" : [ {
|
|
||||||
"type" : "EXTERNAL",
|
|
||||||
"sourceStates" : [ {
|
|
||||||
"rawName" : "OrderStates.SUBMITTED",
|
|
||||||
"fullIdentifier" : "OrderStates.SUBMITTED"
|
|
||||||
} ],
|
|
||||||
"targetStates" : [ {
|
|
||||||
"rawName" : "OrderStates.PAID",
|
|
||||||
"fullIdentifier" : "OrderStates.PAID"
|
|
||||||
} ],
|
|
||||||
"event" : {
|
|
||||||
"rawName" : "OrderEvents.PAY",
|
|
||||||
"fullIdentifier" : "OrderEvents.PAY"
|
|
||||||
},
|
|
||||||
"guard" : null,
|
|
||||||
"actions" : [ ],
|
|
||||||
"order" : null
|
|
||||||
}, {
|
|
||||||
"type" : "EXTERNAL",
|
|
||||||
"sourceStates" : [ {
|
|
||||||
"rawName" : "OrderStates.PAID",
|
|
||||||
"fullIdentifier" : "OrderStates.PAID"
|
|
||||||
} ],
|
|
||||||
"targetStates" : [ {
|
|
||||||
"rawName" : "OrderStates.FULFILLED",
|
|
||||||
"fullIdentifier" : "OrderStates.FULFILLED"
|
|
||||||
} ],
|
|
||||||
"event" : {
|
|
||||||
"rawName" : "OrderEvents.FULFILL",
|
|
||||||
"fullIdentifier" : "OrderEvents.FULFILL"
|
|
||||||
},
|
|
||||||
"guard" : null,
|
|
||||||
"actions" : [ ],
|
|
||||||
"order" : null
|
|
||||||
}, {
|
|
||||||
"type" : "EXTERNAL",
|
|
||||||
"sourceStates" : [ {
|
|
||||||
"rawName" : "OrderStates.SUBMITTED",
|
|
||||||
"fullIdentifier" : "OrderStates.SUBMITTED"
|
|
||||||
} ],
|
|
||||||
"targetStates" : [ {
|
|
||||||
"rawName" : "OrderStates.CANCELED",
|
|
||||||
"fullIdentifier" : "OrderStates.CANCELED"
|
|
||||||
} ],
|
|
||||||
"event" : {
|
|
||||||
"rawName" : "OrderEvents.CANCEL",
|
|
||||||
"fullIdentifier" : "OrderEvents.CANCEL"
|
|
||||||
},
|
|
||||||
"guard" : null,
|
|
||||||
"actions" : [ ],
|
|
||||||
"order" : null
|
|
||||||
}, {
|
|
||||||
"type" : "EXTERNAL",
|
|
||||||
"sourceStates" : [ {
|
|
||||||
"rawName" : "OrderStates.PAID",
|
|
||||||
"fullIdentifier" : "OrderStates.PAID"
|
|
||||||
} ],
|
|
||||||
"targetStates" : [ {
|
|
||||||
"rawName" : "OrderStates.CANCELED",
|
|
||||||
"fullIdentifier" : "OrderStates.CANCELED"
|
|
||||||
} ],
|
|
||||||
"event" : {
|
|
||||||
"rawName" : "OrderEvents.ABCD",
|
|
||||||
"fullIdentifier" : "OrderEvents.ABCD"
|
|
||||||
},
|
|
||||||
"guard" : null,
|
|
||||||
"actions" : [ ],
|
|
||||||
"order" : null
|
|
||||||
} ],
|
|
||||||
"endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ]
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
@startuml
|
|
||||||
!pragma layout smetana
|
|
||||||
set separator none
|
|
||||||
hide empty description
|
|
||||||
hide stereotype
|
|
||||||
skinparam state {
|
|
||||||
BackgroundColor white
|
|
||||||
BorderColor #94a3b8
|
|
||||||
BorderThickness 1
|
|
||||||
FontName Inter
|
|
||||||
FontSize 9
|
|
||||||
FontStyle bold
|
|
||||||
RoundCorner 20
|
|
||||||
Padding 1
|
|
||||||
}
|
|
||||||
skinparam shadowing false
|
|
||||||
skinparam ArrowFontName JetBrains Mono
|
|
||||||
skinparam ArrowFontSize 8
|
|
||||||
skinparam ArrowColor #cbd5e1
|
|
||||||
skinparam ArrowThickness 1
|
|
||||||
skinparam dpi 110
|
|
||||||
skinparam svgLinkTarget _self
|
|
||||||
|
|
||||||
[*] --> OrderStates.SUBMITTED
|
|
||||||
|
|
||||||
|
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID <<external>> : OrderEvents.PAY
|
|
||||||
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.FULFILLED <<external>> : OrderEvents.FULFILL
|
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.CANCEL
|
|
||||||
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.ABCD
|
|
||||||
|
|
||||||
OrderStates.CANCELED --> [*]
|
|
||||||
OrderStates.FULFILLED --> [*]
|
|
||||||
@enduml
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="SUBMITTED">
|
|
||||||
<state id="SUBMITTED">
|
|
||||||
<transition target="PAID" event="OrderEvents.PAY"/>
|
|
||||||
<transition target="CANCELED" event="OrderEvents.CANCEL"/>
|
|
||||||
</state>
|
|
||||||
<state id="PAID">
|
|
||||||
<transition target="FULFILLED" event="OrderEvents.FULFILL"/>
|
|
||||||
<transition target="CANCELED" event="OrderEvents.ABCD"/>
|
|
||||||
</state>
|
|
||||||
<state id="FULFILLED">
|
|
||||||
</state>
|
|
||||||
<state id="CANCELED">
|
|
||||||
</state>
|
|
||||||
</scxml>
|
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -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/
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.config;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
|
|
||||||
public abstract class AbstractBaseConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public String inheritedBeanTester(@Qualifier("customName") QuirkService someService) {
|
|
||||||
someService.doQuirk();
|
|
||||||
return "inherited";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.config;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class BeanParameterTestConfig extends AbstractBaseConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public String myBeanParamTester(@Qualifier("customName") QuirkService someService) {
|
|
||||||
// We will use this method as an entry point in the test
|
|
||||||
someService.doQuirk();
|
|
||||||
return "tested";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.config;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QualifierQuirkService;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class ConcreteReturnTypeConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public QuirkService hiddenConcreteService() {
|
|
||||||
return new QualifierQuirkService(); // It returns an interface, but the concrete type is QualifierQuirkService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.statemachine.config.EnableStateMachine;
|
|
||||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
|
||||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
|
||||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableStateMachine(name = "paymentStateMachine")
|
|
||||||
public class PaymentStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
|
||||||
states
|
|
||||||
.withStates()
|
|
||||||
.initial("NEW")
|
|
||||||
.state("AUTHORIZED")
|
|
||||||
.state("CAPTURED")
|
|
||||||
.state("DECLINED")
|
|
||||||
.state("QUIRK1")
|
|
||||||
.state("QUIRK2")
|
|
||||||
.state("QUIRK3")
|
|
||||||
.state("QUIRK4");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
|
||||||
transitions
|
|
||||||
.withExternal()
|
|
||||||
.source("NEW").target("AUTHORIZED")
|
|
||||||
.event("AUTHORIZE")
|
|
||||||
.and()
|
|
||||||
.withExternal()
|
|
||||||
.source("AUTHORIZED").target("CAPTURED")
|
|
||||||
.event("CAPTURE")
|
|
||||||
.and()
|
|
||||||
.withExternal()
|
|
||||||
.source("NEW").target("DECLINED")
|
|
||||||
.event("DECLINE")
|
|
||||||
.and()
|
|
||||||
.withExternal()
|
|
||||||
.source("NEW").target("QUIRK1")
|
|
||||||
.event("PRIMARY_EVENT")
|
|
||||||
.and()
|
|
||||||
.withExternal()
|
|
||||||
.source("NEW").target("QUIRK2")
|
|
||||||
.event("NAMED_EVENT")
|
|
||||||
.and()
|
|
||||||
.withExternal()
|
|
||||||
.source("NEW").target("QUIRK3")
|
|
||||||
.event("QUALIFIER_EVENT")
|
|
||||||
.and()
|
|
||||||
.withExternal()
|
|
||||||
.source("NEW").target("QUIRK4")
|
|
||||||
.event("FALLBACK_EVENT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AmbiguousA implements AmbiguousService {
|
|
||||||
@Override
|
|
||||||
public void doAmbig() {}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AmbiguousB implements AmbiguousService {
|
|
||||||
@Override
|
|
||||||
public void doAmbig() {}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
public interface AmbiguousService {
|
|
||||||
void doAmbig();
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.statemachine.StateMachine;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class FallbackQuirkService implements QuirkService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("paymentStateMachine")
|
|
||||||
private StateMachine<String, String> stateMachine;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doQuirk() {
|
|
||||||
stateMachine.sendEvent("FALLBACK_EVENT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
|
||||||
public class HighPriorityOrderedService implements OrderedService {
|
|
||||||
@Override
|
|
||||||
public void doAction() {}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Order(10)
|
|
||||||
public class LowPriorityOrderedService implements OrderedService {
|
|
||||||
@Override
|
|
||||||
public void doAction() {}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.statemachine.StateMachine;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service("customName")
|
|
||||||
public class NamedQuirkService implements QuirkService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("paymentStateMachine")
|
|
||||||
private StateMachine<String, String> stateMachine;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doQuirk() {
|
|
||||||
stateMachine.sendEvent("NAMED_EVENT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
public interface OrderedService {
|
|
||||||
void doAction();
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
public interface PaymentService {
|
|
||||||
void processPayment(String paymentId);
|
|
||||||
void capturePayment(String paymentId);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.statemachine.StateMachine;
|
|
||||||
import org.springframework.statemachine.persist.StateMachinePersister;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class PaymentServiceImpl implements PaymentService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("paymentStateMachine")
|
|
||||||
private StateMachine<String, String> stateMachine;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private StateMachinePersister<String, String, String> persister;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processPayment(String paymentId) {
|
|
||||||
// Send a trigger mapped strictly to PaymentStateMachineConfig
|
|
||||||
stateMachine.sendEvent("AUTHORIZE");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void capturePayment(String paymentId) {
|
|
||||||
try {
|
|
||||||
persister.restore(stateMachine, paymentId);
|
|
||||||
} catch (Exception e) {}
|
|
||||||
|
|
||||||
org.springframework.messaging.Message<String> msg = org.springframework.messaging.support.MessageBuilder
|
|
||||||
.withPayload("CAPTURE")
|
|
||||||
.setHeader("paymentId", paymentId)
|
|
||||||
.build();
|
|
||||||
stateMachine.sendEvent(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.context.annotation.Primary;
|
|
||||||
import org.springframework.statemachine.StateMachine;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Primary
|
|
||||||
public class PrimaryQuirkService implements QuirkService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("paymentStateMachine")
|
|
||||||
private StateMachine<String, String> stateMachine;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doQuirk() {
|
|
||||||
stateMachine.sendEvent("PRIMARY_EVENT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.statemachine.StateMachine;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Profile("nonexistent")
|
|
||||||
public class ProfiledQuirkService implements QuirkService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("paymentStateMachine")
|
|
||||||
private StateMachine<String, String> stateMachine;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doQuirk() {
|
|
||||||
stateMachine.sendEvent("PROFILED_EVENT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.statemachine.StateMachine;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class QualifierQuirkService implements QuirkService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("paymentStateMachine")
|
|
||||||
private StateMachine<String, String> stateMachine;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doQuirk() {
|
|
||||||
stateMachine.sendEvent("QUALIFIER_EVENT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.service;
|
|
||||||
|
|
||||||
public interface QuirkService {
|
|
||||||
void doQuirk();
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
|
|
||||||
public interface BaseController {
|
|
||||||
|
|
||||||
@GetMapping("/api/base/{id}")
|
|
||||||
String processBaseEndpoint(@PathVariable String id);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class ConcreteReturnTypeController {
|
|
||||||
|
|
||||||
private final QuirkService hiddenConcreteService;
|
|
||||||
|
|
||||||
public ConcreteReturnTypeController(@Qualifier("hiddenConcreteService") QuirkService hiddenConcreteService) {
|
|
||||||
this.hiddenConcreteService = hiddenConcreteService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/test-concrete")
|
|
||||||
public void testConcrete() {
|
|
||||||
hiddenConcreteService.doQuirk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class FieldInjectionController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("customName")
|
|
||||||
private QuirkService myFieldService;
|
|
||||||
|
|
||||||
@GetMapping("/test-field")
|
|
||||||
public void testField() {
|
|
||||||
myFieldService.doQuirk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.AmbiguousService;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class ListInjectionController {
|
|
||||||
|
|
||||||
private final List<AmbiguousService> services;
|
|
||||||
|
|
||||||
public ListInjectionController(List<AmbiguousService> services) {
|
|
||||||
this.services = services;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/test-list")
|
|
||||||
public void testList() {
|
|
||||||
for (AmbiguousService s : services) {
|
|
||||||
s.doAmbig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.OrderedService;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class OrderedTestController {
|
|
||||||
|
|
||||||
private final OrderedService service;
|
|
||||||
|
|
||||||
// The name "service" does not match HighPriorityOrderedService or LowPriorityOrderedService
|
|
||||||
// So the Fallback by name will fail, and it will fall back to @Order
|
|
||||||
public OrderedTestController(OrderedService service) {
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/test-order")
|
|
||||||
public void testOrder() {
|
|
||||||
service.doAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.PaymentService;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class PaymentController implements BaseController {
|
|
||||||
|
|
||||||
private final PaymentService paymentService;
|
|
||||||
|
|
||||||
public PaymentController(PaymentService paymentService) {
|
|
||||||
this.paymentService = paymentService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String processBaseEndpoint(@PathVariable String id) {
|
|
||||||
paymentService.processPayment(id);
|
|
||||||
return "Started Base Payment: " + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/api/payment/{id}/capture")
|
|
||||||
public String capturePaymentEndpoint(@PathVariable String id) {
|
|
||||||
paymentService.capturePayment(id);
|
|
||||||
return "Captured: " + id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class QuirkController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private QuirkService quirkService; // Should resolve to PrimaryQuirkService
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("customName")
|
|
||||||
private QuirkService someService; // Should resolve to NamedQuirkService
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("qualifierQuirkService")
|
|
||||||
private QuirkService anotherService; // Should resolve to QualifierQuirkService
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private QuirkService fallbackQuirkService; // Should resolve to FallbackQuirkService
|
|
||||||
|
|
||||||
@PostMapping("/api/quirk/primary")
|
|
||||||
public void testPrimary() {
|
|
||||||
quirkService.doQuirk();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/api/quirk/named")
|
|
||||||
public void testNamed() {
|
|
||||||
someService.doQuirk();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/api/quirk/qualifier")
|
|
||||||
public void testQualifier() {
|
|
||||||
anotherService.doQuirk();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/api/quirk/fallback")
|
|
||||||
public void testFallback() {
|
|
||||||
fallbackQuirkService.doQuirk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.extended.web;
|
|
||||||
|
|
||||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class SetterInjectionController {
|
|
||||||
|
|
||||||
private QuirkService setterService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public void setSetterService(@Qualifier("customName") QuirkService setterService) {
|
|
||||||
this.setterService = setterService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/test-setter")
|
|
||||||
public void testSetter() {
|
|
||||||
setterService.doQuirk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
HELP.md
|
|
||||||
.gradle
|
|
||||||
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/
|
|
||||||
!**/src/main/**/out/
|
|
||||||
!**/src/test/**/out/
|
|
||||||
|
|
||||||
### NetBeans ###
|
|
||||||
/nbproject/private/
|
|
||||||
/nbbuild/
|
|
||||||
/dist/
|
|
||||||
/nbdist/
|
|
||||||
/.nb-gradle/
|
|
||||||
|
|
||||||
### VS Code ###
|
|
||||||
.vscode/
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'java'
|
|
||||||
id 'org.springframework.boot' version '3.5.3'
|
|
||||||
id 'io.spring.dependency-management' version '1.1.7'
|
|
||||||
}
|
|
||||||
|
|
||||||
group = 'click.kamil'
|
|
||||||
version = '0.0.1-SNAPSHOT'
|
|
||||||
|
|
||||||
java {
|
|
||||||
toolchain {
|
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
compileOnly {
|
|
||||||
extendsFrom annotationProcessor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter'
|
|
||||||
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
|
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('test') {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
bootJar { enabled = false }
|
|
||||||
jar { enabled = true }
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
|
|
||||||
public class AbcdEvent implements CustomCodeEventInterface {
|
|
||||||
@Override
|
|
||||||
public OrderEvents resolveEventCode() {
|
|
||||||
return OrderEvents.ABCD;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
public interface BaseEvent { OrderEvents getType(); }
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
public class CancelEvent implements BaseEvent {
|
|
||||||
@Override public OrderEvents getType() { return OrderEvents.CANCEL; }
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
|
|
||||||
public interface CustomCodeEventInterface {
|
|
||||||
OrderEvents resolveEventCode();
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
|
|
||||||
public class EventBuilder {
|
|
||||||
public static BaseEvent buildEvent() {
|
|
||||||
return new FulfillEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public BaseEvent buildInstanceEvent() {
|
|
||||||
return new CancelEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
public class FulfillEvent implements BaseEvent {
|
|
||||||
@Override public OrderEvents getType() { return OrderEvents.FULFILL; }
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
|
|
||||||
public class MysteryPayload implements CustomCodeEventInterface {
|
|
||||||
@Override
|
|
||||||
public OrderEvents resolveEventCode() {
|
|
||||||
int a = (int) (Math.random() * 10);
|
|
||||||
return a > 5 ? OrderEvents.ABCD : OrderEvents.PAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
public enum OrderEvents { FULFILL, PAY, CANCEL, ABCD, NOPE, IGNORE }
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
import org.springframework.statemachine.StateMachine;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.messaging.support.MessageBuilder;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class OrderService {
|
|
||||||
private final StateMachine<OrderStates, OrderEvents> sm;
|
|
||||||
|
|
||||||
public OrderService(StateMachine<OrderStates, OrderEvents> sm) { this.sm = sm; }
|
|
||||||
|
|
||||||
public void processEvent(BaseEvent event) {
|
|
||||||
sm.sendEvent(event.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processPayloadEvent(Object payload) {
|
|
||||||
sm.sendEvent(MessageBuilder.withPayload(payload).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processCustomEvent(CustomCodeEventInterface event) {
|
|
||||||
sm.sendEvent(event.resolveEventCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
public enum OrderStates { SUBMITTED, PAID, FULFILLED, CANCELED, PAID1, PAID2, PAID3, INVALID, HAPPEN, SKIPPED, NEVER }
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
public class PayEvent implements BaseEvent {
|
|
||||||
@Override public OrderEvents getType() { return OrderEvents.PAY; }
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
public class PolymorphicController {
|
|
||||||
|
|
||||||
private final OrderService orderService;
|
|
||||||
|
|
||||||
public PolymorphicController(OrderService orderService) {
|
|
||||||
this.orderService = orderService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pay")
|
|
||||||
public void pay() {
|
|
||||||
orderService.processEvent(new PayEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/fulfill")
|
|
||||||
public void fulfill() {
|
|
||||||
orderService.processEvent(new FulfillEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/cancel")
|
|
||||||
public void cancel() {
|
|
||||||
orderService.processEvent(new CancelEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/payload-pay")
|
|
||||||
public void payloadPay() {
|
|
||||||
orderService.processPayloadEvent(new PayEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pay-variable")
|
|
||||||
public void payVariable() {
|
|
||||||
PayEvent event = new PayEvent();
|
|
||||||
orderService.processEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pay-cast")
|
|
||||||
public void payCast() {
|
|
||||||
orderService.processEvent((BaseEvent) new PayEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pay-ternary")
|
|
||||||
public void payTernary(boolean isPay) {
|
|
||||||
orderService.processEvent(isPay ? new PayEvent() : new CancelEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pay-list")
|
|
||||||
public void payList() {
|
|
||||||
java.util.List<BaseEvent> events = java.util.Arrays.asList(new PayEvent(), new FulfillEvent());
|
|
||||||
for (BaseEvent event : events) {
|
|
||||||
orderService.processEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pay-builder-static")
|
|
||||||
public void payBuilderStatic() {
|
|
||||||
orderService.processEvent(EventBuilder.buildEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pay-builder-instance")
|
|
||||||
public void payBuilderInstance() {
|
|
||||||
EventBuilder builder = new EventBuilder();
|
|
||||||
orderService.processEvent(builder.buildInstanceEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/abcd")
|
|
||||||
public void abcd() {
|
|
||||||
orderService.processCustomEvent(new AbcdEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/mystery")
|
|
||||||
public void mystery() {
|
|
||||||
orderService.processCustomEvent(new MysteryPayload());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package click.kamil.examples.statemachine.polymorphic;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.statemachine.config.EnableStateMachine;
|
|
||||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
|
||||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
|
||||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableStateMachine
|
|
||||||
public class PolymorphicStateMachineConfiguration extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
|
||||||
@Override
|
|
||||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
|
||||||
states.withStates().initial(OrderStates.SUBMITTED).state(OrderStates.PAID).state(OrderStates.FULFILLED).state(OrderStates.CANCELED);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
|
||||||
transitions
|
|
||||||
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY).and()
|
|
||||||
.withExternal().source(OrderStates.PAID).target(OrderStates.FULFILLED).event(OrderEvents.FULFILL).and()
|
|
||||||
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELED).event(OrderEvents.CANCEL).and()
|
|
||||||
.withExternal().source(OrderStates.PAID).target(OrderStates.CANCELED).event(OrderEvents.ABCD);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user