Compare commits
13 Commits
ai-branch
...
db58c55b84
| Author | SHA1 | Date | |
|---|---|---|---|
| db58c55b84 | |||
| f9e6247eb3 | |||
| 76ac143370 | |||
| 9ca8b2fe37 | |||
| 56cdf96f2d | |||
| c37aa92c78 | |||
| 3ac057618f | |||
| d59f4ac166 | |||
| 19547d3c5f | |||
| ab37eb5d40 | |||
| bf9208d529 | |||
| 7077214c81 | |||
| 344e295106 |
1
TODO.md
1
TODO.md
@@ -1,6 +1,5 @@
|
|||||||
1.extract from all possible formats (ask chatgpt)
|
1.extract from all possible formats (ask chatgpt)
|
||||||
|
|
||||||
[DONE] json
|
|
||||||
[TODO] xml, maybe yaml
|
[TODO] xml, maybe yaml
|
||||||
|
|
||||||
2. [PLAN] Support external dependencies (JARs/compiled classes):
|
2. [PLAN] Support external dependencies (JARs/compiled classes):
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ dependencies {
|
|||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1'
|
||||||
|
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.46'
|
compileOnly 'org.projectlombok:lombok:1.18.46'
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.46'
|
annotationProcessor 'org.projectlombok:lombok:1.18.46'
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import java.util.List;
|
|||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// Enable diagnostic mode early before SLF4J initializes
|
||||||
|
for (String arg : args) {
|
||||||
|
if ("--debug".equals(arg)) {
|
||||||
|
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Manual DI / Wiring
|
// Manual DI / Wiring
|
||||||
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
||||||
var exportService = new ExportService(exporters);
|
var exportService = new ExportService(exporters);
|
||||||
|
|||||||
@@ -41,37 +41,75 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
if (t.getEvent() != null) {
|
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();
|
||||||
String smEvent = simplify(smEventRaw);
|
String smEvent = simplify(smEventRaw);
|
||||||
if (smEvent.equals(triggerEvent)) {
|
|
||||||
// Event matches. Check source state if provided
|
boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") ||
|
||||||
|
triggerEvent.equals("msg") || triggerEvent.equals("message") ||
|
||||||
|
triggerEvent.equals("payload");
|
||||||
|
|
||||||
|
if (isWildcard) {
|
||||||
|
String targetVar = chain.getContextMachineId();
|
||||||
|
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||||
|
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||||
|
}
|
||||||
|
// We no longer hard-block wildcards without a specific routing context.
|
||||||
|
// If a project doesn't use standard SM persisters (e.g. restores state manually),
|
||||||
|
// contextMachineId will be null. We should still link the wildcard to provide SOME visibility,
|
||||||
|
// rather than completely hiding the endpoint.
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> polyEvents = tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : java.util.Collections.emptyList();
|
||||||
|
boolean hasPolyMatch = false;
|
||||||
|
for (String pe : polyEvents) {
|
||||||
|
String simplePe = pe;
|
||||||
|
if (pe.contains(".")) {
|
||||||
|
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
|
||||||
|
hasPolyMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent)))) {
|
||||||
|
// Event matches or is a wildcard
|
||||||
for (State smSourceState : t.getSourceStates()) {
|
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)) {
|
||||||
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
|
.sourceState(smSourceRaw)
|
||||||
|
.targetState(smSourceRaw)
|
||||||
|
.event(smEventRaw)
|
||||||
|
.build();
|
||||||
|
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||||
|
matched.add(mt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for (State smTargetState : t.getTargetStates()) {
|
for (State smTargetState : t.getTargetStates()) {
|
||||||
String sourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
|
||||||
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
||||||
matched.add(MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
.sourceState(sourceRaw)
|
.sourceState(smSourceRaw)
|
||||||
.targetState(targetRaw)
|
.targetState(targetRaw)
|
||||||
.event(smEventRaw)
|
.event(smEventRaw)
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new CallChain with the matched transitions
|
|
||||||
CallChain updatedChain = CallChain.builder()
|
|
||||||
.entryPoint(chain.getEntryPoint())
|
|
||||||
.methodChain(chain.getMethodChain())
|
|
||||||
.triggerPoint(chain.getTriggerPoint())
|
|
||||||
.contextMachineId(chain.getContextMachineId())
|
|
||||||
.matchedTransitions(matched)
|
|
||||||
.build();
|
.build();
|
||||||
|
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||||
|
matched.add(mt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updatedChains.add(updatedChain);
|
if (!matched.isEmpty()) {
|
||||||
|
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
|
||||||
|
updatedChains.add(newChain);
|
||||||
|
} else {
|
||||||
|
updatedChains.add(chain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the metadata with the new call chains
|
// Update the metadata with the new call chains
|
||||||
@@ -85,12 +123,196 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
result.setMetadata(updatedMetadata);
|
result.setMetadata(updatedMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||||
|
String beanId = chain.getTriggerPoint() != null ? chain.getTriggerPoint().getStateMachineId() : null;
|
||||||
|
|
||||||
|
if (beanId != null && !beanId.isEmpty() && currentMachineName != null) {
|
||||||
|
// currentMachineName format: FQN (Entry Point) or FQN#beanMethodName
|
||||||
|
if (currentMachineName.equals(beanId) || currentMachineName.endsWith("#" + beanId)) {
|
||||||
|
return true; // Exact match!
|
||||||
|
}
|
||||||
|
|
||||||
|
String simplifiedMachineName = currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
String simplifiedBeanId = beanId.substring(beanId.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
|
||||||
|
// If bean was explicitly identified but doesn't match the current machine, try to reject it
|
||||||
|
if (!simplifiedMachineName.contains(simplifiedBeanId) && !simplifiedBeanId.contains(simplifiedMachineName)) {
|
||||||
|
String beanPrefix = getFirstCamelCaseWord(beanId.substring(beanId.lastIndexOf('.') + 1));
|
||||||
|
String machinePrefix = getFirstCamelCaseWord(currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1));
|
||||||
|
if (beanPrefix != null && machinePrefix != null && !beanPrefix.equalsIgnoreCase(machinePrefix)) {
|
||||||
|
// e.g. PaymentStateMachineFactory vs OrderStateMachineConfig
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic approach combining Prefix, Package, and String-distance heuristics
|
||||||
|
// without relying on a hardcoded list of magic keywords.
|
||||||
|
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
|
||||||
|
String smPackage = getPackageName(currentMachineName);
|
||||||
|
String smSimple = getSimpleClassName(currentMachineName);
|
||||||
|
String smPrefix = getFirstCamelCaseWord(smSimple);
|
||||||
|
|
||||||
|
boolean hasPositiveMatch = false;
|
||||||
|
boolean hasStrongMismatch = false;
|
||||||
|
|
||||||
|
for (String method : chain.getMethodChain()) {
|
||||||
|
String chainClass = getClassNameOnly(method);
|
||||||
|
String chainPackage = getPackageName(chainClass);
|
||||||
|
String chainSimple = getSimpleClassName(chainClass);
|
||||||
|
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
||||||
|
|
||||||
|
// 1. Explicit Prefix Match (e.g. ElectronicsOrderService & ElectronicsStateMachine)
|
||||||
|
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
|
||||||
|
hasPositiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Explicit Sub-Package Match (e.g. com.company.electronics.service & com.company.electronics.sm)
|
||||||
|
// We consider it a match if they share a package segment beyond the first 2 (which are usually com.company)
|
||||||
|
String deepShared = getDeepSharedPackage(smPackage, chainPackage);
|
||||||
|
if (deepShared != null) {
|
||||||
|
hasPositiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if there is a strong mismatch.
|
||||||
|
// A strong mismatch happens if the chain class has a VERY distinct prefix/package
|
||||||
|
// that contradicts the SM's distinct prefix/package.
|
||||||
|
// Without magic keywords, we guess if a prefix is distinct if it's long enough and doesn't match.
|
||||||
|
if (smPrefix != null && chainPrefix != null && !smPrefix.equals(chainPrefix)) {
|
||||||
|
// They have different first CamelCase words.
|
||||||
|
// This could be Electronics vs ComputerStore, or it could be Common vs ComputerStore.
|
||||||
|
// If they diverge at a deep package level, it's a strong mismatch.
|
||||||
|
if (isDeepPackageDivergence(smPackage, chainPackage)) {
|
||||||
|
hasStrongMismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a positive structural match, route it!
|
||||||
|
if (hasPositiveMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no positive match, but we detected a strong structural mismatch, reject it!
|
||||||
|
if (hasStrongMismatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If neither, we allow it (fallback for common/shared endpoints without distinct domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPackageName(String fqn) {
|
||||||
|
if (fqn == null || !fqn.contains(".")) return "";
|
||||||
|
return fqn.substring(0, fqn.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleClassName(String fqn) {
|
||||||
|
if (fqn == null) return "";
|
||||||
|
if (!fqn.contains(".")) return fqn;
|
||||||
|
return fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClassNameOnly(String methodFqn) {
|
||||||
|
if (methodFqn == null) return "";
|
||||||
|
String clean = methodFqn;
|
||||||
|
if (clean.contains("(")) {
|
||||||
|
clean = clean.substring(0, clean.indexOf('('));
|
||||||
|
}
|
||||||
|
if (clean.contains(".")) {
|
||||||
|
clean = clean.substring(0, clean.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFirstCamelCaseWord(String simpleName) {
|
||||||
|
if (simpleName == null || simpleName.isEmpty()) return null;
|
||||||
|
// e.g. "ElectronicsOrderService" -> "Electronics"
|
||||||
|
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
|
||||||
|
if (words.length > 0) {
|
||||||
|
return words[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeepSharedPackage(String pkg1, String pkg2) {
|
||||||
|
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null;
|
||||||
|
String[] p1 = pkg1.split("\\.");
|
||||||
|
String[] p2 = pkg2.split("\\.");
|
||||||
|
|
||||||
|
// Find the deepest common segment index
|
||||||
|
int matchIdx = -1;
|
||||||
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
|
if (p1[i].equals(p2[i])) {
|
||||||
|
matchIdx = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If they share at least 3 segments (e.g. com.company.electronics), it's a deep match
|
||||||
|
if (matchIdx >= 2) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for(int i = 0; i <= matchIdx; i++) {
|
||||||
|
sb.append(p1[i]).append(".");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDeepPackageDivergence(String pkg1, String pkg2) {
|
||||||
|
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return false;
|
||||||
|
String[] p1 = pkg1.split("\\.");
|
||||||
|
String[] p2 = pkg2.split("\\.");
|
||||||
|
|
||||||
|
// If they share exactly the root (com.company) but diverge at the 3rd segment (e.g. electronics vs computerstore)
|
||||||
|
// Then it's a divergence.
|
||||||
|
if (p1.length >= 3 && p2.length >= 3) {
|
||||||
|
return p1[0].equals(p2[0]) && p1[1].equals(p2[1]) && !p1[2].equals(p2[2]);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private String simplify(String name) {
|
private String simplify(String name) {
|
||||||
if (name == null) return "";
|
if (name == null) return null;
|
||||||
int dot = name.lastIndexOf('.');
|
// Strip common suffixes
|
||||||
if (dot >= 0) {
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
return name.substring(dot + 1);
|
if (simplified.isEmpty()) {
|
||||||
|
simplified = name;
|
||||||
}
|
}
|
||||||
return name;
|
// Simplify full identifiers to just the last part (enum name)
|
||||||
|
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
|
||||||
|
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
|
||||||
|
return simplified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||||
|
// Only fire if it looks like a simple identifier (no spaces, no parens)
|
||||||
|
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
|
||||||
|
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
|
||||||
|
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
|
||||||
|
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
|
||||||
|
|
||||||
|
shorter = shorter.toLowerCase();
|
||||||
|
longer = longer.toLowerCase();
|
||||||
|
|
||||||
|
if (longer.contains(shorter)) {
|
||||||
|
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
|
||||||
|
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
|
||||||
|
if (shorter.length() < 4) {
|
||||||
|
return longer.matches(".*(^|_|-)" + shorter + "(_|-|$).*");
|
||||||
|
}
|
||||||
|
// For longer strings, a straight contains is usually safe enough
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder(toBuilder = true)
|
||||||
@Jacksonized
|
@Jacksonized
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class CallChain {
|
public class CallChain {
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ import java.util.List;
|
|||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class LibraryHint {
|
public class LibraryHint {
|
||||||
private final String methodFqn; // e.g., "com.thirdparty.Workflow.send"
|
private final String methodFqn; // e.g., "com.thirdparty.Workflow.send"
|
||||||
private final String event; // The event it triggers
|
private final String event; // The event it triggers (static)
|
||||||
|
private final Integer eventArgumentIndex; // e.g., 0 to extract from the 0th argument
|
||||||
|
private final String eventArgumentMethod; // e.g., "getType" to extract from argument method call
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,7 @@ public class TriggerPoint {
|
|||||||
private final String stateMachineId; // Optional: to link to a specific SM instance
|
private final String stateMachineId; // Optional: to link to a specific SM instance
|
||||||
private final String sourceState; // Optional: if we can determine the expected current state
|
private final String sourceState; // Optional: if we can determine the expected current state
|
||||||
private final int lineNumber;
|
private final int lineNumber;
|
||||||
|
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
||||||
|
@Builder.Default
|
||||||
|
private final Map<String, String> payloadTypes = new java.util.HashMap<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,13 +48,33 @@ public class ConstantResolver {
|
|||||||
return resolveInfix(infix, context, visited);
|
return resolveInfix(infix, context, visited);
|
||||||
}
|
}
|
||||||
if (expr instanceof QualifiedName qn) {
|
if (expr instanceof QualifiedName qn) {
|
||||||
return resolveManual(qn, context, visited);
|
String val = resolveManual(qn, context, visited);
|
||||||
|
return val != null ? val : qn.toString();
|
||||||
}
|
}
|
||||||
if (expr instanceof SimpleName sn) {
|
if (expr instanceof SimpleName sn) {
|
||||||
return resolveManual(sn, context, visited);
|
return resolveManual(sn, context, visited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) {
|
||||||
|
String val = resolveInternal(mi.getExpression(), context, visited);
|
||||||
|
if (val != null) {
|
||||||
|
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
|
||||||
|
if (val.contains(".")) {
|
||||||
|
return val.substring(val.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for unresolved AST nodes
|
||||||
|
if (mi.getExpression() instanceof QualifiedName qn) {
|
||||||
|
return qn.getName().getIdentifier();
|
||||||
|
} else if (mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IMethodBinding mb = mi.resolveMethodBinding();
|
IMethodBinding mb = mi.resolveMethodBinding();
|
||||||
if (mb != null) {
|
if (mb != null) {
|
||||||
ITypeBinding returnType = mb.getReturnType();
|
ITypeBinding returnType = mb.getReturnType();
|
||||||
@@ -160,12 +180,44 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: Check constructors for assignments
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.isConstructor() && md.getBody() != null) {
|
||||||
|
String val = findAssignmentInBlock(md.getBody(), fieldName, context, visited);
|
||||||
|
if (val != null) return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
visited.remove(fieldId);
|
visited.remove(fieldId);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String findAssignmentInBlock(Block block, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
|
if (block == null) return null;
|
||||||
|
for (Object stmtObj : block.statements()) {
|
||||||
|
if (stmtObj instanceof ExpressionStatement exprStmt) {
|
||||||
|
if (exprStmt.getExpression() instanceof Assignment assignment) {
|
||||||
|
Expression left = assignment.getLeftHandSide();
|
||||||
|
boolean match = false;
|
||||||
|
if (left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) {
|
||||||
|
match = true;
|
||||||
|
} else if (left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) {
|
||||||
|
if (fa.getExpression() instanceof ThisExpression) {
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private String findValueFromAnnotation(FieldDeclaration fd) {
|
private String findValueFromAnnotation(FieldDeclaration fd) {
|
||||||
for (Object mod : fd.modifiers()) {
|
for (Object mod : fd.modifiers()) {
|
||||||
if (mod instanceof Annotation ann) {
|
if (mod instanceof Annotation ann) {
|
||||||
|
|||||||
@@ -105,25 +105,32 @@ public class PropertyResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> loadYaml(Path path) {
|
private Map<String, String> loadYaml(Path path) {
|
||||||
// Placeholder for future YAML support
|
Map<String, String> props = new HashMap<>();
|
||||||
log.warn("YAML parsing not fully implemented yet for {}", path);
|
try {
|
||||||
return new HashMap<>();
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(new com.fasterxml.jackson.dataformat.yaml.YAMLFactory());
|
||||||
|
Map<String, Object> map = mapper.readValue(path.toFile(), new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
|
||||||
|
flattenYaml("", map, props);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to load YAML from {}", path);
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void flattenYaml(String prefix, Map<String, Object> map, Map<String, String> result) {
|
||||||
|
if (map == null) return;
|
||||||
|
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||||
|
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof Map) {
|
||||||
|
flattenYaml(key, (Map<String, Object>) value, result);
|
||||||
|
} else if (value != null) {
|
||||||
|
result.put(key, value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String resolveValue(String placeholder, Map<String, String> properties) {
|
public String resolveValue(String placeholder, Map<String, String> properties) {
|
||||||
if (placeholder == null || !placeholder.contains("${")) return placeholder;
|
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(placeholder, properties);
|
||||||
|
|
||||||
// Very basic placeholder extraction: ${key} or ${key:default}
|
|
||||||
String content = placeholder.substring(placeholder.indexOf("${") + 2, placeholder.lastIndexOf("}"));
|
|
||||||
String key = content;
|
|
||||||
String defaultValue = null;
|
|
||||||
|
|
||||||
if (content.contains(":")) {
|
|
||||||
int colonIndex = content.indexOf(":");
|
|
||||||
key = content.substring(0, colonIndex);
|
|
||||||
defaultValue = content.substring(colonIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties.getOrDefault(key, defaultValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.util.*;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class CallGraphBuilder {
|
public class CallGraphBuilder {
|
||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
|
private final click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver springDependencyResolver;
|
||||||
private final ConstantResolver constantResolver;
|
private final ConstantResolver constantResolver;
|
||||||
private String currentMethodFqn;
|
private String currentMethodFqn;
|
||||||
private Map<String, List<CallEdge>> graph;
|
private Map<String, List<CallEdge>> graph;
|
||||||
@@ -24,7 +25,12 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CallGraphBuilder(CodebaseContext context) {
|
public CallGraphBuilder(CodebaseContext context) {
|
||||||
|
this(context, new click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver(context, new click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry(context)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallGraphBuilder(CodebaseContext context, click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver springDependencyResolver) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.springDependencyResolver = springDependencyResolver;
|
||||||
this.constantResolver = new ConstantResolver();
|
this.constantResolver = new ConstantResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,10 +40,12 @@ public class CallGraphBuilder {
|
|||||||
|
|
||||||
for (EntryPoint ep : entryPoints) {
|
for (EntryPoint ep : entryPoints) {
|
||||||
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
||||||
|
boolean foundAny = false;
|
||||||
for (TriggerPoint tp : triggers) {
|
for (TriggerPoint tp : triggers) {
|
||||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||||
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
|
foundAny = true;
|
||||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
||||||
String contextMachineId = extractContextMachineId(path, callGraph);
|
String contextMachineId = extractContextMachineId(path, callGraph);
|
||||||
chains.add(CallChain.builder()
|
chains.add(CallChain.builder()
|
||||||
@@ -48,6 +56,9 @@ public class CallGraphBuilder {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!foundAny && log.isDebugEnabled()) {
|
||||||
|
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return chains;
|
return chains;
|
||||||
}
|
}
|
||||||
@@ -60,6 +71,17 @@ public class CallGraphBuilder {
|
|||||||
|
|
||||||
String currentParamName = event;
|
String currentParamName = event;
|
||||||
String resolvedValue = event;
|
String resolvedValue = event;
|
||||||
|
String methodSuffix = "";
|
||||||
|
|
||||||
|
// Extract method calls like .getType() so we can trace the base parameter
|
||||||
|
int dotIndex = currentParamName.indexOf('.');
|
||||||
|
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
|
||||||
|
char nextChar = currentParamName.charAt(dotIndex + 1);
|
||||||
|
if (Character.isLowerCase(nextChar)) {
|
||||||
|
methodSuffix = currentParamName.substring(dotIndex);
|
||||||
|
currentParamName = currentParamName.substring(0, dotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Walk backwards up the call chain
|
// Walk backwards up the call chain
|
||||||
for (int i = path.size() - 1; i > 0; i--) {
|
for (int i = path.size() - 1; i > 0; i--) {
|
||||||
@@ -68,6 +90,21 @@ public class CallGraphBuilder {
|
|||||||
|
|
||||||
// Find parameter index in target method
|
// Find parameter index in target method
|
||||||
int paramIndex = getParameterIndex(target, currentParamName);
|
int paramIndex = getParameterIndex(target, currentParamName);
|
||||||
|
if (paramIndex < 0) {
|
||||||
|
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
|
||||||
|
String tracedVar = traceLocalVariable(target, currentParamName);
|
||||||
|
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||||
|
// Extract method calls like .getType() from the traced variable
|
||||||
|
int dotIdx = tracedVar.indexOf('.');
|
||||||
|
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||||
|
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||||
|
tracedVar = tracedVar.substring(0, dotIdx);
|
||||||
|
}
|
||||||
|
currentParamName = tracedVar;
|
||||||
|
resolvedValue = tracedVar + methodSuffix;
|
||||||
|
paramIndex = getParameterIndex(target, currentParamName);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (paramIndex < 0) {
|
if (paramIndex < 0) {
|
||||||
break; // Parameter name changed or not found, stop tracing
|
break; // Parameter name changed or not found, stop tracing
|
||||||
}
|
}
|
||||||
@@ -81,8 +118,14 @@ public class CallGraphBuilder {
|
|||||||
if (paramIndex < edge.getArguments().size()) {
|
if (paramIndex < edge.getArguments().size()) {
|
||||||
String arg = edge.getArguments().get(paramIndex);
|
String arg = edge.getArguments().get(paramIndex);
|
||||||
if (arg != null) {
|
if (arg != null) {
|
||||||
|
// If the argument passed has a method call, extract it
|
||||||
|
int dotIdx = arg.indexOf('.');
|
||||||
|
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
|
||||||
|
methodSuffix = arg.substring(dotIdx) + methodSuffix;
|
||||||
|
arg = arg.substring(0, dotIdx);
|
||||||
|
}
|
||||||
currentParamName = arg;
|
currentParamName = arg;
|
||||||
resolvedValue = arg;
|
resolvedValue = arg + methodSuffix;
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -93,13 +136,159 @@ public class CallGraphBuilder {
|
|||||||
if (!found) break; // Could not map argument
|
if (!found) break; // Could not map argument
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resolvedValue.equals(event)) {
|
// Final check on the entry method
|
||||||
|
String entryMethod = path.get(0);
|
||||||
|
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
|
||||||
|
if (entryParamIndex < 0) {
|
||||||
|
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
|
||||||
|
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||||
|
int dotIdx = tracedVar.indexOf('.');
|
||||||
|
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||||
|
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||||
|
tracedVar = tracedVar.substring(0, dotIdx);
|
||||||
|
}
|
||||||
|
currentParamName = tracedVar;
|
||||||
|
resolvedValue = tracedVar + methodSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> polymorphicEvents = new ArrayList<>();
|
||||||
|
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
|
||||||
|
int lastDot = resolvedValue.lastIndexOf('.');
|
||||||
|
int firstDot = resolvedValue.indexOf('.');
|
||||||
|
int openParen = resolvedValue.indexOf('(', lastDot);
|
||||||
|
|
||||||
|
if (lastDot > 0 && openParen > lastDot) {
|
||||||
|
String varName = resolvedValue.substring(0, firstDot);
|
||||||
|
if (varName.contains("(")) {
|
||||||
|
varName = null;
|
||||||
|
}
|
||||||
|
String methodName = resolvedValue.substring(lastDot + 1, openParen);
|
||||||
|
|
||||||
|
String declaredType = null;
|
||||||
|
String sourceMethod = null;
|
||||||
|
|
||||||
|
if (varName != null) {
|
||||||
|
for (String methodFqn : path) {
|
||||||
|
declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||||
|
if (declaredType != null) {
|
||||||
|
sourceMethod = methodFqn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String baseExpr = resolvedValue.substring(0, lastDot);
|
||||||
|
if (baseExpr.contains("new ")) {
|
||||||
|
int newIdx = baseExpr.indexOf("new ");
|
||||||
|
int openParenBase = baseExpr.indexOf('(', newIdx);
|
||||||
|
if (openParenBase > newIdx) {
|
||||||
|
declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim();
|
||||||
|
if (declaredType.contains("<")) {
|
||||||
|
declaredType = declaredType.substring(0, declaredType.indexOf('<'));
|
||||||
|
}
|
||||||
|
sourceMethod = "inline-instantiation";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (declaredType != null) {
|
||||||
|
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
|
||||||
|
List<String> typesToInspect = new ArrayList<>();
|
||||||
|
if ("inline-instantiation".equals(sourceMethod)) {
|
||||||
|
typesToInspect.add(declaredType);
|
||||||
|
} else {
|
||||||
|
typesToInspect.add(declaredType);
|
||||||
|
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> resolvedBeans = springDependencyResolver.resolve(declaredType, null, varName);
|
||||||
|
if (resolvedBeans.isEmpty()) {
|
||||||
|
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||||
|
} else {
|
||||||
|
for (click.kamil.springstatemachineexporter.analysis.spring.SpringBean bean : resolvedBeans) {
|
||||||
|
if (!bean.getTypeFqn().equals(declaredType)) {
|
||||||
|
typesToInspect.add(bean.getTypeFqn());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
|
||||||
|
|
||||||
|
for (String type : typesToInspect) {
|
||||||
|
Set<String> visited = new HashSet<>();
|
||||||
|
TypeDeclaration baseTd = context.getTypeDeclaration(type);
|
||||||
|
CompilationUnit cuToUse = null;
|
||||||
|
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
|
||||||
|
cuToUse = (CompilationUnit) baseTd.getRoot();
|
||||||
|
} else {
|
||||||
|
String firstPathMethod = path.get(0);
|
||||||
|
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
|
||||||
|
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
|
||||||
|
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
|
||||||
|
cuToUse = (CompilationUnit) entryTd.getRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
|
||||||
|
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
|
||||||
|
for (String constant : constants) {
|
||||||
|
if (!polymorphicEvents.contains(constant)) {
|
||||||
|
polymorphicEvents.add(constant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant
|
||||||
|
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
|
||||||
|
// for string literals or enum-like constants.
|
||||||
|
boolean hasValidConstant = false;
|
||||||
|
for (String ev : polymorphicEvents) {
|
||||||
|
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
|
||||||
|
if (val.equals(val.toUpperCase()) && val.length() > 2) {
|
||||||
|
hasValidConstant = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidConstant && resolvedValue.contains("(")) {
|
||||||
|
java.util.List<String> scraped = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
// Extract "STRING_LITERALS"
|
||||||
|
java.util.regex.Matcher m1 = java.util.regex.Pattern.compile("\"([^\"]+)\"").matcher(resolvedValue);
|
||||||
|
while (m1.find()) {
|
||||||
|
scraped.add(m1.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract ENUM_LIKE_CONSTANTS
|
||||||
|
java.util.regex.Matcher m2 = java.util.regex.Pattern.compile("\\b([A-Z_]{3,})\\b").matcher(resolvedValue);
|
||||||
|
while (m2.find()) {
|
||||||
|
if (!scraped.contains(m2.group(1))) {
|
||||||
|
scraped.add(m2.group(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scraped.isEmpty()) {
|
||||||
|
polymorphicEvents.clear();
|
||||||
|
polymorphicEvents.addAll(scraped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones
|
||||||
|
if (polymorphicEvents.size() > 1) {
|
||||||
|
polymorphicEvents.removeIf(e -> {
|
||||||
|
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
|
||||||
|
return !val.equals(val.toUpperCase()) || val.length() <= 2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
.event(resolvedValue)
|
.event(resolvedValue)
|
||||||
.className(tp.getClassName())
|
.className(tp.getClassName())
|
||||||
.methodName(tp.getMethodName())
|
.methodName(tp.getMethodName())
|
||||||
.sourceFile(tp.getSourceFile())
|
.sourceFile(tp.getSourceFile())
|
||||||
.lineNumber(tp.getLineNumber())
|
.lineNumber(tp.getLineNumber())
|
||||||
|
.polymorphicEvents(polymorphicEvents)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +314,181 @@ public class CallGraphBuilder {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getVariableDeclaredType(String methodFqn, String varName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null) {
|
||||||
|
for (Object pObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return svd.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String[] foundType = new String[1];
|
||||||
|
if (md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
foundType[0] = node.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return foundType[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||||
|
if (depth > 20) return Collections.emptyList();
|
||||||
|
String fqn = className + "." + methodName;
|
||||||
|
if (!visited.add(fqn)) return Collections.emptyList();
|
||||||
|
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||||
|
if (tempTd == null) {
|
||||||
|
tempTd = context.getTypeDeclaration(className);
|
||||||
|
}
|
||||||
|
final TypeDeclaration td = tempTd;
|
||||||
|
if (td == null) {
|
||||||
|
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
|
||||||
|
}
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
Expression retExpr = node.getExpression();
|
||||||
|
if (retExpr != null) {
|
||||||
|
boolean handled = false;
|
||||||
|
if (retExpr instanceof MethodInvocation mi) {
|
||||||
|
// Follow delegation first
|
||||||
|
String called = resolveCalledMethod(mi);
|
||||||
|
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
|
||||||
|
if (called != null && called.contains(".")) {
|
||||||
|
if (visited.contains(called)) {
|
||||||
|
handled = true;
|
||||||
|
} else {
|
||||||
|
String cName = called.substring(0, called.lastIndexOf('.'));
|
||||||
|
String mName = called.substring(called.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||||
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
String val = constantResolver.resolve(retExpr, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
constants.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
constants.add(val);
|
||||||
|
}
|
||||||
|
} else if (retExpr instanceof QualifiedName qn) {
|
||||||
|
constants.add(qn.toString());
|
||||||
|
} else if (retExpr instanceof SimpleName sn) {
|
||||||
|
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
constants.addAll(consts);
|
||||||
|
} else {
|
||||||
|
constants.add(sn.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(fqn);
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String traceLocalVariable(String methodFqn, String varName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
final Expression[] initializer = new Expression[1];
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
initializer[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initializer[0] != null) {
|
||||||
|
Expression expr = traceVariable(initializer[0]);
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
|
||||||
|
Expression innerMost = unwrapMethodInvocation(mi, 0);
|
||||||
|
if (innerMost instanceof MethodInvocation innerMi) {
|
||||||
|
if (innerMi.getExpression() instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
|
||||||
|
}
|
||||||
|
return innerMi.getName().getIdentifier() + "()";
|
||||||
|
}
|
||||||
|
if (innerMost instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return innerMost.toString();
|
||||||
|
}
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return expr.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
||||||
|
if (depth > 5) return mi;
|
||||||
|
if (!mi.arguments().isEmpty()) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
for (String node : path) {
|
for (String node : path) {
|
||||||
List<CallEdge> edges = callGraph.get(node);
|
List<CallEdge> edges = callGraph.get(node);
|
||||||
@@ -143,22 +507,26 @@ public class CallGraphBuilder {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (MethodDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, List<CallEdge>> buildCallGraph() {
|
private Map<String, List<CallEdge>> buildCallGraph() {
|
||||||
graph = new HashMap<>();
|
graph = new HashMap<>();
|
||||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||||
cu.accept(new ASTVisitor() {
|
cu.accept(new ASTVisitor() {
|
||||||
@Override
|
|
||||||
public boolean visit(MethodDeclaration node) {
|
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
|
||||||
if (td != null) {
|
|
||||||
currentMethodFqn = context.getFqn(td) + "." + node.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(MethodInvocation node) {
|
public boolean visit(MethodInvocation node) {
|
||||||
if (currentMethodFqn != null) {
|
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> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||||
List<String> args = resolveArguments(node.arguments());
|
List<String> args = resolveArguments(node.arguments());
|
||||||
for (String calledMethod : calledMethods) {
|
for (String calledMethod : calledMethods) {
|
||||||
@@ -168,13 +536,45 @@ public class CallGraphBuilder {
|
|||||||
if (argObj instanceof ExpressionMethodReference emr) {
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
String typeName = emr.getExpression().toString();
|
String typeName = emr.getExpression().toString();
|
||||||
if ("this".equals(typeName) || "super".equals(typeName)) {
|
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
TypeDeclaration td2 = findEnclosingType(node);
|
||||||
if (td != null) {
|
if (td2 != null) {
|
||||||
String refMethod = resolveMethodInType(td, emr.getName().getIdentifier());
|
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||||
if (refMethod != null) {
|
if (refMethod != null) {
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
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));
|
||||||
|
|
||||||
|
String receiverName = null;
|
||||||
|
if (emr.getExpression() instanceof SimpleName sn) {
|
||||||
|
receiverName = sn.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> resolvedBeans = springDependencyResolver.resolve(fallbackTypeFqn, null, receiverName);
|
||||||
|
if (resolvedBeans.isEmpty()) {
|
||||||
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
|
for (String impl : impls) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (click.kamil.springstatemachineexporter.analysis.spring.SpringBean bean : resolvedBeans) {
|
||||||
|
if (!bean.getTypeFqn().equals(fallbackTypeFqn)) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(bean.getTypeFqn() + "." + emr.getName().getIdentifier(), args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +584,11 @@ public class CallGraphBuilder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(SuperMethodInvocation node) {
|
public boolean visit(SuperMethodInvocation node) {
|
||||||
if (currentMethodFqn != null) {
|
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();
|
String methodName = node.getName().getIdentifier();
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
@@ -203,6 +607,7 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return super.visit(node);
|
return super.visit(node);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -246,10 +651,16 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||||
expr = traceVariable(expr);
|
Expression tracedExpr = traceVariable(expr);
|
||||||
if (expr instanceof ClassInstanceCreation cic) {
|
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
||||||
if (!cic.arguments().isEmpty()) {
|
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||||
expr = (Expression) cic.arguments().get(0);
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,21 +678,91 @@ public class CallGraphBuilder {
|
|||||||
if (baseCalled == null) return Collections.emptyList();
|
if (baseCalled == null) return Collections.emptyList();
|
||||||
|
|
||||||
List<String> allResolved = new ArrayList<>();
|
List<String> allResolved = new ArrayList<>();
|
||||||
allResolved.add(baseCalled);
|
|
||||||
|
|
||||||
if (baseCalled.contains(".")) {
|
if (baseCalled.contains(".")) {
|
||||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||||
|
String receiverName = null;
|
||||||
|
if (node.getExpression() instanceof SimpleName sn) {
|
||||||
|
receiverName = sn.getIdentifier();
|
||||||
|
} else if (node.getExpression() instanceof FieldAccess fa) {
|
||||||
|
receiverName = fa.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
String qualifier = extractQualifierFromVariable(node, receiverName);
|
||||||
|
|
||||||
|
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> resolvedBeans = springDependencyResolver.resolve(className, qualifier, receiverName);
|
||||||
|
|
||||||
|
if (resolvedBeans.isEmpty()) {
|
||||||
|
allResolved.add(baseCalled);
|
||||||
List<String> impls = context.getImplementations(className);
|
List<String> impls = context.getImplementations(className);
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
allResolved.add(impl + "." + methodName);
|
allResolved.add(impl + "." + methodName);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (click.kamil.springstatemachineexporter.analysis.spring.SpringBean bean : resolvedBeans) {
|
||||||
|
allResolved.add(bean.getTypeFqn() + "." + methodName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allResolved.add(baseCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allResolved;
|
return allResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractQualifierFromVariable(ASTNode node, String varName) {
|
||||||
|
if (varName == null) return null;
|
||||||
|
|
||||||
|
MethodDeclaration md = findEnclosingMethod(node);
|
||||||
|
if (md != null) {
|
||||||
|
for (Object paramObj : md.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return extractQualifierFromModifiers(svd.modifiers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment vdf) {
|
||||||
|
if (vdf.getName().getIdentifier().equals(varName)) {
|
||||||
|
return extractQualifierFromModifiers(fd.modifiers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractQualifierFromModifiers(List<?> modifiers) {
|
||||||
|
for (Object modObj : modifiers) {
|
||||||
|
if (modObj instanceof NormalAnnotation na) {
|
||||||
|
if ("Qualifier".equals(na.getTypeName().getFullyQualifiedName()) || "org.springframework.beans.factory.annotation.Qualifier".equals(na.getTypeName().getFullyQualifiedName())) {
|
||||||
|
for (Object valObj : na.values()) {
|
||||||
|
if (valObj instanceof MemberValuePair mvp) {
|
||||||
|
if ("value".equals(mvp.getName().getIdentifier()) && mvp.getValue() instanceof StringLiteral sl) {
|
||||||
|
return sl.getLiteralValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (modObj instanceof SingleMemberAnnotation sma) {
|
||||||
|
if ("Qualifier".equals(sma.getTypeName().getFullyQualifiedName()) || "org.springframework.beans.factory.annotation.Qualifier".equals(sma.getTypeName().getFullyQualifiedName())) {
|
||||||
|
if (sma.getValue() instanceof StringLiteral sl) {
|
||||||
|
return sl.getLiteralValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveCalledMethod(MethodInvocation node) {
|
private String resolveCalledMethod(MethodInvocation node) {
|
||||||
Expression receiver = node.getExpression();
|
Expression receiver = node.getExpression();
|
||||||
String methodName = node.getName().getIdentifier();
|
String methodName = node.getName().getIdentifier();
|
||||||
@@ -299,7 +780,18 @@ public class CallGraphBuilder {
|
|||||||
return binding.getQualifiedName() + "." + methodName;
|
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) {
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||||
|
if (fallbackTypeFqn != null) {
|
||||||
|
return fallbackTypeFqn + "." + methodName;
|
||||||
|
}
|
||||||
String receiverName = sn.getIdentifier();
|
String receiverName = sn.getIdentifier();
|
||||||
return receiverName + "." + methodName;
|
return receiverName + "." + methodName;
|
||||||
}
|
}
|
||||||
@@ -307,6 +799,85 @@ public class CallGraphBuilder {
|
|||||||
return null;
|
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) {
|
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
if (md != null) {
|
if (md != null) {
|
||||||
@@ -336,15 +907,35 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Path search dead-end at {} when looking for {}", start, target);
|
||||||
|
}
|
||||||
|
|
||||||
visited.remove(start);
|
visited.remove(start);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isHeuristicMatch(String neighbor, String target) {
|
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||||
|
if (neighbor.equals(target)) return true;
|
||||||
if (target.endsWith("." + neighbor)) return true;
|
if (target.endsWith("." + neighbor)) return true;
|
||||||
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
|
||||||
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
int nIdx = neighbor.lastIndexOf('.');
|
||||||
return simpleNeighbor.equals(simpleTarget);
|
int tIdx = target.lastIndexOf('.');
|
||||||
|
if (nIdx > 0 && tIdx > 0) {
|
||||||
|
String nClass = neighbor.substring(0, nIdx);
|
||||||
|
String nMethod = neighbor.substring(nIdx + 1);
|
||||||
|
String tClass = target.substring(0, tIdx);
|
||||||
|
String tMethod = target.substring(tIdx + 1);
|
||||||
|
|
||||||
|
if (nMethod.equals(tMethod)) {
|
||||||
|
List<String> impls = context.getImplementations(nClass);
|
||||||
|
if (impls.contains(tClass)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
@@ -384,11 +975,112 @@ public class CallGraphBuilder {
|
|||||||
return expr;
|
return expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
private List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
ASTNode parent = node.getParent();
|
List<String> results = new ArrayList<>();
|
||||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
|
||||||
parent = parent.getParent();
|
// 1. Check Field Declarations
|
||||||
|
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
|
||||||
|
Expression right = frag.getInitializer();
|
||||||
|
if (right instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(right, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
}
|
}
|
||||||
return (MethodDeclaration) parent;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.ASTVisitor assignmentVisitor = new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression left = node.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
|
||||||
|
|
||||||
|
Expression right = node.getRightHandSide();
|
||||||
|
if (right instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
|
||||||
|
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
|
||||||
|
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
|
||||||
|
} else if (mi.getExpression() instanceof SimpleName) {
|
||||||
|
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(right, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation node) {
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
Expression arg = (Expression) argObj;
|
||||||
|
String val = constantResolver.resolve(arg, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation node) {
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
Expression arg = (Expression) argObj;
|
||||||
|
String val = constantResolver.resolve(arg, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Check Constructors
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.isConstructor() && md.getBody() != null) {
|
||||||
|
md.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Initializers
|
||||||
|
for (Object bodyDecl : td.bodyDeclarations()) {
|
||||||
|
if (bodyDecl instanceof org.eclipse.jdt.core.dom.Initializer init && init.getBody() != null) {
|
||||||
|
init.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,11 @@ public class GenericEventDetector {
|
|||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
private final ConstantResolver constantResolver;
|
private final ConstantResolver constantResolver;
|
||||||
private final List<LibraryHint> hints;
|
private final List<LibraryHint> hints;
|
||||||
|
private final click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver springDependencyResolver;
|
||||||
|
|
||||||
|
public GenericEventDetector(CodebaseContext context, ConstantResolver constantResolver, List<LibraryHint> hints) {
|
||||||
|
this(context, constantResolver, hints, new click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver(context, new click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry(context)));
|
||||||
|
}
|
||||||
|
|
||||||
public List<TriggerPoint> detect(CompilationUnit cu) {
|
public List<TriggerPoint> detect(CompilationUnit cu) {
|
||||||
List<TriggerPoint> triggers = new ArrayList<>();
|
List<TriggerPoint> triggers = new ArrayList<>();
|
||||||
@@ -64,7 +69,18 @@ public class GenericEventDetector {
|
|||||||
|
|
||||||
for (LibraryHint hint : hints) {
|
for (LibraryHint hint : hints) {
|
||||||
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
|
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
|
||||||
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, hint.getEvent());
|
String eventToUse = hint.getEvent();
|
||||||
|
if (eventToUse == null && hint.getEventArgumentIndex() != null) {
|
||||||
|
if (node.arguments().size() > hint.getEventArgumentIndex()) {
|
||||||
|
Expression argExpr = (Expression) node.arguments().get(hint.getEventArgumentIndex());
|
||||||
|
eventToUse = argExpr.toString();
|
||||||
|
if (hint.getEventArgumentMethod() != null && !hint.getEventArgumentMethod().isEmpty()) {
|
||||||
|
eventToUse = eventToUse + "." + hint.getEventArgumentMethod() + "()";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, eventToUse);
|
||||||
if (builtTriggers != null) {
|
if (builtTriggers != null) {
|
||||||
for (TriggerPoint trigger : builtTriggers) {
|
for (TriggerPoint trigger : builtTriggers) {
|
||||||
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||||
@@ -131,6 +147,28 @@ public class GenericEventDetector {
|
|||||||
|
|
||||||
String sourceState = extractSourceState(node);
|
String sourceState = extractSourceState(node);
|
||||||
|
|
||||||
|
String stateMachineId = null;
|
||||||
|
if (node.getExpression() instanceof SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
String className = resolveReceiverTypeFallback(sn, cu);
|
||||||
|
if (className != null && springDependencyResolver != null) {
|
||||||
|
List<click.kamil.springstatemachineexporter.analysis.spring.SpringBean> beans = springDependencyResolver.resolve(className, null, varName);
|
||||||
|
if (beans.size() == 1) {
|
||||||
|
if (!beans.get(0).getBeanNames().isEmpty()) {
|
||||||
|
stateMachineId = beans.get(0).getBeanNames().iterator().next();
|
||||||
|
} else {
|
||||||
|
stateMachineId = beans.get(0).getTypeFqn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Map<String, String> payloadTypes = java.util.Collections.emptyMap();
|
||||||
|
if (forcedEvent == null && !node.arguments().isEmpty()) {
|
||||||
|
Expression eventExpr = (Expression) node.arguments().get(0);
|
||||||
|
payloadTypes = extractPayloadsFromMessageBuilder(eventExpr, cu);
|
||||||
|
}
|
||||||
|
|
||||||
List<TriggerPoint> results = new ArrayList<>();
|
List<TriggerPoint> results = new ArrayList<>();
|
||||||
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||||
String[] parts = eventValue.substring(9).split(",");
|
String[] parts = eventValue.substring(9).split(",");
|
||||||
@@ -139,10 +177,12 @@ public class GenericEventDetector {
|
|||||||
results.add(TriggerPoint.builder()
|
results.add(TriggerPoint.builder()
|
||||||
.event(part.trim())
|
.event(part.trim())
|
||||||
.sourceState(sourceState)
|
.sourceState(sourceState)
|
||||||
|
.stateMachineId(stateMachineId)
|
||||||
.className(context.getFqn(type))
|
.className(context.getFqn(type))
|
||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.payloadTypes(payloadTypes)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,10 +190,12 @@ public class GenericEventDetector {
|
|||||||
results.add(TriggerPoint.builder()
|
results.add(TriggerPoint.builder()
|
||||||
.event(eventValue)
|
.event(eventValue)
|
||||||
.sourceState(sourceState)
|
.sourceState(sourceState)
|
||||||
|
.stateMachineId(stateMachineId)
|
||||||
.className(context.getFqn(type))
|
.className(context.getFqn(type))
|
||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.payloadTypes(payloadTypes)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +257,78 @@ public class GenericEventDetector {
|
|||||||
return expr.toString();
|
return expr.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private java.util.Map<String, String> extractPayloadsFromMessageBuilder(Expression expr, CompilationUnit cu) {
|
||||||
|
if (expr instanceof CastExpression ce) {
|
||||||
|
return extractPayloadsFromMessageBuilder(ce.getExpression(), cu);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
final Expression[] initializer = new Expression[1];
|
||||||
|
enclosingMethod.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
initializer[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (initializer[0] != null) {
|
||||||
|
return extractPayloadsFromMessageBuilder(initializer[0], cu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(expr instanceof MethodInvocation mi)) return java.util.Collections.emptyMap();
|
||||||
|
|
||||||
|
java.util.Map<String, String> payloads = new java.util.HashMap<>();
|
||||||
|
MethodInvocation current = mi;
|
||||||
|
while (current != null) {
|
||||||
|
String name = current.getName().getIdentifier();
|
||||||
|
if ("setHeader".equals(name) && current.arguments().size() == 2) {
|
||||||
|
Expression keyExpr = (Expression) current.arguments().get(0);
|
||||||
|
Expression valueExpr = (Expression) current.arguments().get(1);
|
||||||
|
|
||||||
|
String key = constantResolver.resolve(keyExpr, context);
|
||||||
|
if (key == null) {
|
||||||
|
if (keyExpr instanceof StringLiteral sl) key = sl.getLiteralValue();
|
||||||
|
else key = keyExpr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String type = null;
|
||||||
|
if (valueExpr instanceof SimpleName vSn) {
|
||||||
|
type = resolveReceiverTypeFallback(vSn, cu);
|
||||||
|
}
|
||||||
|
if (type == null) type = "Object";
|
||||||
|
|
||||||
|
// Keep simple type name if possible
|
||||||
|
if (type.contains(".")) {
|
||||||
|
type = type.substring(type.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
payloads.put(key, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression receiver = current.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation nextMi) {
|
||||||
|
current = nextMi;
|
||||||
|
} else {
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payloads;
|
||||||
|
}
|
||||||
|
|
||||||
private String extractEventFromMessageBuilder(Expression expr) {
|
private String extractEventFromMessageBuilder(Expression expr) {
|
||||||
if (expr instanceof CastExpression ce) {
|
if (expr instanceof CastExpression ce) {
|
||||||
return extractEventFromMessageBuilder(ce.getExpression());
|
return extractEventFromMessageBuilder(ce.getExpression());
|
||||||
@@ -291,7 +405,8 @@ public class GenericEventDetector {
|
|||||||
} else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
|
} else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
|
||||||
// If the event is obtained by calling a method on a provider/supplier parameter,
|
// If the event is obtained by calling a method on a provider/supplier parameter,
|
||||||
// return the provider's name so CallGraphBuilder can trace the lambda argument
|
// return the provider's name so CallGraphBuilder can trace the lambda argument
|
||||||
return sn.getIdentifier();
|
String traced = extractEventFromMessageBuilder(sn);
|
||||||
|
return traced != null ? traced : sn.getIdentifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression receiver = current.getExpression();
|
Expression receiver = current.getExpression();
|
||||||
@@ -304,6 +419,77 @@ public class GenericEventDetector {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveReceiverTypeFallback(SimpleName receiverNameNode, CompilationUnit cu) {
|
||||||
|
String varName = receiverNameNode.getIdentifier();
|
||||||
|
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return resolveTypeToFqn(svd.getType(), cu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enclosingMethod.getBody() != null) {
|
||||||
|
Type[] foundType = new Type[1];
|
||||||
|
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
foundType[0] = node.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundType[0] != null) {
|
||||||
|
return resolveTypeToFqn(foundType[0], cu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||||
|
if (enclosingType != null) {
|
||||||
|
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||||
|
for (Object fragObj : field.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
return resolveTypeToFqn(field.getType(), cu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
ASTNode parent = node.getParent();
|
ASTNode parent = node.getParent();
|
||||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,16 +27,26 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
private final CallGraphBuilder callGraphBuilder;
|
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 SpringBeanRegistry springBeanRegistry;
|
||||||
|
private final SpringDependencyResolver springDependencyResolver;
|
||||||
|
|
||||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.rootDir = rootDir;
|
this.rootDir = rootDir;
|
||||||
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
|
||||||
|
// Initialize Spring Bean Context early
|
||||||
|
this.springBeanRegistry = new click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry(context);
|
||||||
|
this.springBeanRegistry.discoverBeans();
|
||||||
|
this.springDependencyResolver = new click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver(context, springBeanRegistry);
|
||||||
|
|
||||||
|
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints(), springDependencyResolver);
|
||||||
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
||||||
this.messagingDetector = new MessagingDetector(context);
|
this.messagingDetector = new MessagingDetector(context);
|
||||||
this.callGraphBuilder = new CallGraphBuilder(context);
|
this.callGraphBuilder = new CallGraphBuilder(context, springDependencyResolver);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -58,6 +71,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
allEntryPoints.addAll(mvcDetector.detect(cu));
|
allEntryPoints.addAll(mvcDetector.detect(cu));
|
||||||
allEntryPoints.addAll(messagingDetector.detect(cu));
|
allEntryPoints.addAll(messagingDetector.detect(cu));
|
||||||
allEntryPoints.addAll(interceptorDetector.detect(cu));
|
allEntryPoints.addAll(interceptorDetector.detect(cu));
|
||||||
|
allEntryPoints.addAll(componentDetector.detect(cu));
|
||||||
}
|
}
|
||||||
log.info("Found {} entry points (including interceptors and listeners) in total", allEntryPoints.size());
|
log.info("Found {} entry points (including interceptors and listeners) in total", allEntryPoints.size());
|
||||||
return allEntryPoints;
|
return allEntryPoints;
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
|
import org.eclipse.jdt.core.dom.Annotation;
|
||||||
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
import org.eclipse.jdt.core.dom.MemberValuePair;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.NormalAnnotation;
|
||||||
|
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SpringComponentDetector {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public List<EntryPoint> detect(CompilationUnit cu) {
|
||||||
|
List<EntryPoint> entryPoints = new ArrayList<>();
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodDeclaration node) {
|
||||||
|
if (!(node.getParent() instanceof TypeDeclaration)) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
TypeDeclaration parentTd = (TypeDeclaration) node.getParent();
|
||||||
|
|
||||||
|
for (Object modifier : node.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
if (typeName.endsWith("Scheduled")) {
|
||||||
|
Map<String, String> meta = new HashMap<>();
|
||||||
|
String cron = extractAnnotationValue(annotation, "cron");
|
||||||
|
if (!cron.isEmpty())
|
||||||
|
meta.put("cron", cron);
|
||||||
|
String fixedRate = extractAnnotationValue(annotation, "fixedRate");
|
||||||
|
if (!fixedRate.isEmpty())
|
||||||
|
meta.put("fixedRate", fixedRate);
|
||||||
|
String fixedDelay = extractAnnotationValue(annotation, "fixedDelay");
|
||||||
|
if (!fixedDelay.isEmpty())
|
||||||
|
meta.put("fixedDelay", fixedDelay);
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("@Scheduled: " + node.getName().getIdentifier())
|
||||||
|
.className(context.getFqn(parentTd))
|
||||||
|
.methodName(node.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||||
|
.metadata(meta)
|
||||||
|
.build());
|
||||||
|
} else if (typeName.endsWith("EventListener")) {
|
||||||
|
Map<String, String> meta = new HashMap<>();
|
||||||
|
String classes = extractAnnotationValue(annotation, "classes");
|
||||||
|
if (!classes.isEmpty())
|
||||||
|
meta.put("classes", classes);
|
||||||
|
String condition = extractAnnotationValue(annotation, "condition");
|
||||||
|
if (!condition.isEmpty())
|
||||||
|
meta.put("condition", condition);
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("@EventListener: " + node.getName().getIdentifier())
|
||||||
|
.className(context.getFqn(parentTd))
|
||||||
|
.methodName(node.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||||
|
.metadata(meta)
|
||||||
|
.build());
|
||||||
|
} else if (typeName.endsWith("Around") || typeName.endsWith("Before") || typeName.endsWith("After") || typeName.endsWith("AfterReturning") || typeName.endsWith("AfterThrowing")) {
|
||||||
|
Map<String, String> meta = new HashMap<>();
|
||||||
|
String value = extractAnnotationValue(annotation, "value");
|
||||||
|
if (!value.isEmpty())
|
||||||
|
meta.put("pointcut", value);
|
||||||
|
else {
|
||||||
|
String pointcut = extractAnnotationValue(annotation, "pointcut");
|
||||||
|
if (!pointcut.isEmpty())
|
||||||
|
meta.put("pointcut", pointcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("@" + annotation.getTypeName().getFullyQualifiedName() + ": " + node.getName().getIdentifier())
|
||||||
|
.className(context.getFqn(parentTd))
|
||||||
|
.methodName(node.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||||
|
.metadata(meta)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return entryPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractAnnotationValue(Annotation annotation, String memberName) {
|
||||||
|
if (annotation instanceof SingleMemberAnnotation sma) {
|
||||||
|
if ("value".equals(memberName)) {
|
||||||
|
return sma.getValue().toString();
|
||||||
|
}
|
||||||
|
} else if (annotation instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if (pair.getName().getIdentifier().equals(memberName)) {
|
||||||
|
return pair.getValue().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class SpringBean {
|
||||||
|
/**
|
||||||
|
* The fully qualified name of the bean's actual type.
|
||||||
|
*/
|
||||||
|
private String typeFqn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names under which this bean is registered (could be multiple if aliases exist).
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private Set<String> beanNames = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The qualifiers associated with this bean.
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private Set<String> qualifiers = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the bean is annotated with @Primary.
|
||||||
|
*/
|
||||||
|
private boolean isPrimary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FQN of the class where this bean is declared (e.g. the @Configuration class).
|
||||||
|
*/
|
||||||
|
private String declaringClassFqn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method name if the bean was created via @Bean. Null if it's a component.
|
||||||
|
*/
|
||||||
|
private String factoryMethodName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class SpringBeanRegistry {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final List<SpringBean> beans = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final Set<String> STEREOTYPES = Set.of(
|
||||||
|
"Component", "Service", "Repository", "Controller", "RestController", "Configuration"
|
||||||
|
);
|
||||||
|
|
||||||
|
public SpringBeanRegistry(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void discoverBeans() {
|
||||||
|
for (TypeDeclaration td : context.getTypeDeclarations()) {
|
||||||
|
if (td.isInterface()) continue; // Interfaces themselves aren't beans unless they have @Bean methods, but let's stick to classes for now.
|
||||||
|
|
||||||
|
// 1. Check if the class itself is a bean (Stereotype annotations)
|
||||||
|
if (isStereotypeAnnotated(td)) {
|
||||||
|
registerClassBean(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check for @Bean methods inside the class
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (hasBeanAnnotation(md)) {
|
||||||
|
registerMethodBean(td, md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerClassBean(TypeDeclaration td) {
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (fqn == null) return;
|
||||||
|
|
||||||
|
Set<String> beanNames = new HashSet<>();
|
||||||
|
Set<String> qualifiers = new HashSet<>();
|
||||||
|
boolean isPrimary = false;
|
||||||
|
|
||||||
|
String defaultName = getDefaultBeanName(td.getName().getIdentifier());
|
||||||
|
|
||||||
|
// Extract names and qualifiers from annotations
|
||||||
|
for (Object modObj : td.modifiers()) {
|
||||||
|
if (modObj instanceof Annotation ann) {
|
||||||
|
String annName = ann.getTypeName().getFullyQualifiedName();
|
||||||
|
String simpleName = getSimpleName(annName);
|
||||||
|
|
||||||
|
if (STEREOTYPES.contains(simpleName)) {
|
||||||
|
String value = extractValue(ann);
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
beanNames.add(value);
|
||||||
|
}
|
||||||
|
} else if ("Qualifier".equals(simpleName)) {
|
||||||
|
String value = extractValue(ann);
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
qualifiers.add(value);
|
||||||
|
}
|
||||||
|
} else if ("Primary".equals(simpleName)) {
|
||||||
|
isPrimary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beanNames.isEmpty()) {
|
||||||
|
beanNames.add(defaultName);
|
||||||
|
}
|
||||||
|
|
||||||
|
beans.add(SpringBean.builder()
|
||||||
|
.typeFqn(fqn)
|
||||||
|
.beanNames(beanNames)
|
||||||
|
.qualifiers(qualifiers)
|
||||||
|
.isPrimary(isPrimary)
|
||||||
|
.declaringClassFqn(fqn)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerMethodBean(TypeDeclaration td, MethodDeclaration md) {
|
||||||
|
Type returnType = md.getReturnType2();
|
||||||
|
if (returnType == null) return;
|
||||||
|
|
||||||
|
String declaredTypeFqn = resolveTypeToFqn(returnType, td);
|
||||||
|
if (declaredTypeFqn == null) return;
|
||||||
|
|
||||||
|
Set<String> beanNames = new HashSet<>();
|
||||||
|
Set<String> qualifiers = new HashSet<>();
|
||||||
|
boolean isPrimary = false;
|
||||||
|
|
||||||
|
String defaultName = md.getName().getIdentifier();
|
||||||
|
|
||||||
|
for (Object modObj : md.modifiers()) {
|
||||||
|
if (modObj instanceof Annotation ann) {
|
||||||
|
String annName = ann.getTypeName().getFullyQualifiedName();
|
||||||
|
String simpleName = getSimpleName(annName);
|
||||||
|
|
||||||
|
if ("Bean".equals(simpleName)) {
|
||||||
|
List<String> values = extractArrayValues(ann);
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
beanNames.addAll(values);
|
||||||
|
}
|
||||||
|
} else if ("Qualifier".equals(simpleName)) {
|
||||||
|
String value = extractValue(ann);
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
qualifiers.add(value);
|
||||||
|
}
|
||||||
|
} else if ("Primary".equals(simpleName)) {
|
||||||
|
isPrimary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beanNames.isEmpty()) {
|
||||||
|
beanNames.add(defaultName);
|
||||||
|
}
|
||||||
|
|
||||||
|
beans.add(SpringBean.builder()
|
||||||
|
.typeFqn(declaredTypeFqn)
|
||||||
|
.beanNames(beanNames)
|
||||||
|
.qualifiers(qualifiers)
|
||||||
|
.isPrimary(isPrimary)
|
||||||
|
.declaringClassFqn(context.getFqn(td))
|
||||||
|
.factoryMethodName(md.getName().getIdentifier())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isStereotypeAnnotated(TypeDeclaration td) {
|
||||||
|
return checkStereotypeAnnotated(td, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkStereotypeAnnotated(TypeDeclaration td, Set<String> visited) {
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (fqn != null) {
|
||||||
|
if (!visited.add(fqn)) return false; // Cycle detection
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object modObj : td.modifiers()) {
|
||||||
|
if (modObj instanceof Annotation ann) {
|
||||||
|
String simpleName = getSimpleName(ann.getTypeName().getFullyQualifiedName());
|
||||||
|
if (STEREOTYPES.contains(simpleName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could check superclasses/interfaces if we want to support inherited annotations,
|
||||||
|
// but typically Spring Boot relies on annotations on the concrete class for components.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasBeanAnnotation(MethodDeclaration md) {
|
||||||
|
for (Object modObj : md.modifiers()) {
|
||||||
|
if (modObj instanceof Annotation ann) {
|
||||||
|
String simpleName = getSimpleName(ann.getTypeName().getFullyQualifiedName());
|
||||||
|
if ("Bean".equals(simpleName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleName(String fqn) {
|
||||||
|
return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDefaultBeanName(String className) {
|
||||||
|
if (className == null || className.isEmpty()) return className;
|
||||||
|
return Character.toLowerCase(className.charAt(0)) + className.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractValue(Annotation ann) {
|
||||||
|
if (ann instanceof SingleMemberAnnotation sma) {
|
||||||
|
return stripQuotes(sma.getValue().toString());
|
||||||
|
} else if (ann instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if ("value".equals(pair.getName().getIdentifier()) || "name".equals(pair.getName().getIdentifier())) {
|
||||||
|
return stripQuotes(pair.getValue().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> extractArrayValues(Annotation ann) {
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
if (ann instanceof SingleMemberAnnotation sma) {
|
||||||
|
Expression expr = sma.getValue();
|
||||||
|
if (expr instanceof ArrayInitializer arr) {
|
||||||
|
for (Object item : arr.expressions()) {
|
||||||
|
results.add(stripQuotes(item.toString()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(stripQuotes(expr.toString()));
|
||||||
|
}
|
||||||
|
} else if (ann instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if ("value".equals(pair.getName().getIdentifier()) || "name".equals(pair.getName().getIdentifier())) {
|
||||||
|
Expression expr = pair.getValue();
|
||||||
|
if (expr instanceof ArrayInitializer arr) {
|
||||||
|
for (Object item : arr.expressions()) {
|
||||||
|
results.add(stripQuotes(item.toString()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(stripQuotes(expr.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripQuotes(String s) {
|
||||||
|
if (s == null) return null;
|
||||||
|
return s.replaceAll("^\"|\"$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(Type type, TypeDeclaration contextTd) {
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextTd);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) contextTd.getRoot();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to import matching
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SpringBean> getBeans() {
|
||||||
|
return Collections.unmodifiableList(beans);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class SpringDependencyResolver {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final SpringBeanRegistry registry;
|
||||||
|
|
||||||
|
public SpringDependencyResolver(CodebaseContext context, SpringBeanRegistry registry) {
|
||||||
|
this.context = context;
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the exact SpringBean that would be injected for a given injection point.
|
||||||
|
*
|
||||||
|
* @param requiredTypeFqn The FQN of the type being injected.
|
||||||
|
* @param qualifier The value of the @Qualifier annotation (if any).
|
||||||
|
* @param injectionName The name of the field or parameter being injected (used as fallback).
|
||||||
|
* @return A list of resolved beans. Usually 1. If 0, unresolved. If > 1, ambiguous or a Collection injection.
|
||||||
|
*/
|
||||||
|
public List<SpringBean> resolve(String requiredTypeFqn, String qualifier, String injectionName) {
|
||||||
|
if (requiredTypeFqn == null) return new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. Gather all acceptable types (the required type itself + all implementations)
|
||||||
|
Set<String> acceptableTypes = new HashSet<>();
|
||||||
|
acceptableTypes.add(requiredTypeFqn);
|
||||||
|
acceptableTypes.addAll(context.getImplementations(requiredTypeFqn));
|
||||||
|
|
||||||
|
// 2. Filter by Type
|
||||||
|
List<SpringBean> candidates = registry.getBeans().stream()
|
||||||
|
.filter(bean -> acceptableTypes.contains(bean.getTypeFqn()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (candidates.isEmpty() || candidates.size() == 1) {
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Filter by Qualifier (if provided)
|
||||||
|
if (qualifier != null && !qualifier.isEmpty()) {
|
||||||
|
List<SpringBean> qualifiedCandidates = candidates.stream()
|
||||||
|
.filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (qualifiedCandidates.size() == 1) {
|
||||||
|
return qualifiedCandidates;
|
||||||
|
} else if (!qualifiedCandidates.isEmpty()) {
|
||||||
|
candidates = qualifiedCandidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Filter by @Primary
|
||||||
|
List<SpringBean> primaryCandidates = candidates.stream()
|
||||||
|
.filter(SpringBean::isPrimary)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (primaryCandidates.size() == 1) {
|
||||||
|
return primaryCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Fallback to Injection Name (field name or parameter name)
|
||||||
|
if (injectionName != null && !injectionName.isEmpty()) {
|
||||||
|
List<SpringBean> namedCandidates = candidates.stream()
|
||||||
|
.filter(bean -> bean.getBeanNames().contains(injectionName))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (namedCandidates.size() == 1) {
|
||||||
|
return namedCandidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return whatever candidates remain (could be ambiguous)
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,6 +94,10 @@ public class CodebaseContext {
|
|||||||
this.propertyResolver.setActiveProfiles(profiles);
|
this.propertyResolver.setActiveProfiles(profiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getActiveProfiles() {
|
||||||
|
return Collections.unmodifiableList(activeProfiles);
|
||||||
|
}
|
||||||
|
|
||||||
public String resolveString(String input) {
|
public String resolveString(String input) {
|
||||||
if (input == null) return null;
|
if (input == null) return null;
|
||||||
|
|
||||||
@@ -213,10 +217,19 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inheritance Mapping
|
// Inheritance Mapping
|
||||||
for (Object itf : td.superInterfaceTypes()) {
|
// Track implementations (for both interfaces and superclasses)
|
||||||
String itfName = itf.toString();
|
if (td.superInterfaceTypes() != null) {
|
||||||
|
for (Object itfObj : td.superInterfaceTypes()) {
|
||||||
|
Type itf = (Type) itfObj;
|
||||||
|
String itfName = extractTypeName(itf);
|
||||||
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Type superclass = td.getSuperclassType();
|
||||||
|
if (superclass != null) {
|
||||||
|
String superName = extractTypeName(superclass);
|
||||||
|
interfaceToImpls.computeIfAbsent(superName, k -> new ArrayList<>()).add(fqn);
|
||||||
|
}
|
||||||
|
|
||||||
// Recursively index nested types
|
// Recursively index nested types
|
||||||
for (Object type : td.getTypes()) {
|
for (Object type : td.getTypes()) {
|
||||||
@@ -227,28 +240,38 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getImplementations(String interfaceName) {
|
public List<String> getImplementations(String interfaceName) {
|
||||||
|
Set<String> allImpls = new HashSet<>();
|
||||||
|
collectImplementations(interfaceName, allImpls, new HashSet<>());
|
||||||
|
return new ArrayList<>(allImpls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
||||||
|
if (!visited.add(typeName)) return;
|
||||||
|
|
||||||
// Try direct match
|
// Try direct match
|
||||||
List<String> impls = interfaceToImpls.get(interfaceName);
|
List<String> directImpls = interfaceToImpls.get(typeName);
|
||||||
if (impls != null) return impls;
|
|
||||||
|
|
||||||
// Try FQN match if input was simple name
|
// Try FQN match if input was simple name
|
||||||
String fqn = simpleNameToFqn.get(interfaceName);
|
if (directImpls == null) {
|
||||||
if (fqn != null && interfaceToImpls.containsKey(fqn)) {
|
String fqn = simpleNameToFqn.get(typeName);
|
||||||
return interfaceToImpls.get(fqn);
|
if (fqn != null) {
|
||||||
|
directImpls = interfaceToImpls.get(fqn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try simple name match if input was FQN
|
// Try simple name match if input was FQN
|
||||||
if (interfaceName.contains(".")) {
|
if (directImpls == null && typeName.contains(".")) {
|
||||||
String simpleName = interfaceName.substring(interfaceName.lastIndexOf('.') + 1);
|
String simpleName = typeName.substring(typeName.lastIndexOf('.') + 1);
|
||||||
List<String> simpleImpls = interfaceToImpls.get(simpleName);
|
directImpls = interfaceToImpls.get(simpleName);
|
||||||
if (simpleImpls != null) {
|
|
||||||
// To be perfectly safe, we could check if simpleNameToFqn matches,
|
|
||||||
// but for call graphs, returning potential implementations is fine.
|
|
||||||
return simpleImpls;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Collections.emptyList();
|
if (directImpls != null) {
|
||||||
|
for (String impl : directImpls) {
|
||||||
|
results.add(impl);
|
||||||
|
// Recursively find implementations of the implementation (e.g., subclasses of an abstract class)
|
||||||
|
collectImplementations(impl, results, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
|
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
|
||||||
|
|||||||
@@ -54,11 +54,19 @@ public class ExporterCommand implements Callable<Integer> {
|
|||||||
@Option(names = {"--state"}, description = "Format for states when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
|
@Option(names = {"--state"}, description = "Format for states when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
|
||||||
private click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat;
|
private click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat;
|
||||||
|
|
||||||
|
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
||||||
|
private boolean debug;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
var out = spec.commandLine().getOut();
|
var out = spec.commandLine().getOut();
|
||||||
var err = spec.commandLine().getErr();
|
var err = spec.commandLine().getErr();
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||||
|
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,cyan Diagnostic mode enabled|@"));
|
||||||
|
}
|
||||||
|
|
||||||
if (inputDir == null && jsonFile == null) {
|
if (inputDir == null && jsonFile == null) {
|
||||||
inputDir = Path.of(".");
|
inputDir = Path.of(".");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ public class ExportService {
|
|||||||
new EntryPointEnricher(),
|
new EntryPointEnricher(),
|
||||||
new PropertyEnricher(),
|
new PropertyEnricher(),
|
||||||
new CallChainEnricher(),
|
new CallChainEnricher(),
|
||||||
|
(result, context, intelligence) -> {
|
||||||
|
// Force property resolution before transition linking
|
||||||
|
if (result.getMetadata() != null && result.getMetadata().getProperties() != null) {
|
||||||
|
Map<String, Map<String, String>> allProps = result.getMetadata().getProperties();
|
||||||
|
Map<String, String> merged = new HashMap<>();
|
||||||
|
if (allProps.containsKey("default")) merged.putAll(allProps.get("default"));
|
||||||
|
for (String profile : context.getActiveProfiles()) {
|
||||||
|
if (allProps.containsKey(profile)) merged.putAll(allProps.get(profile));
|
||||||
|
}
|
||||||
|
result.applyResolution(merged);
|
||||||
|
}
|
||||||
|
},
|
||||||
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ import java.util.List;
|
|||||||
public class GoldenUpdater {
|
public class GoldenUpdater {
|
||||||
|
|
||||||
private static final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
private static final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
||||||
private static final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
|
private static final ExportService exportService = new ExportService(exporters);
|
||||||
private static final ExportService exportService = new ExportService(exporters, enrichmentService);
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
List<TestScenario> scenarios = provideTestScenarios();
|
List<TestScenario> scenarios = provideTestScenarios();
|
||||||
|
|||||||
@@ -118,4 +118,264 @@ class TransitionLinkerEnricherTest {
|
|||||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
assertThat(updatedChain.getMatchedTransitions()).isNull();
|
assertThat(updatedChain.getMatchedTransitions()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchWildcardVariable() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||||
|
t1.setEvent(Event.of("PAY", "PAY"));
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("event").build())
|
||||||
|
.contextMachineId("testMachine")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("testMachine")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotMatchArbitraryGetXMethodWildcard() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||||
|
t1.setEvent(Event.of("PAY", "PAY"));
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("richEvent.getId()").build())
|
||||||
|
.contextMachineId("testMachine")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("testMachine")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChain.getMatchedTransitions()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAllowWildcardWhenContextMachineIdIsMissing() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||||
|
t1.setEvent(Event.of("PAY", "PAY"));
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("event").build())
|
||||||
|
// NO contextMachineId or stateMachineId
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("testMachine")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFalsePositiveOnSubstringContains() {
|
||||||
|
// This test proves that the heuristic is entirely removed and false positives are eliminated
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||||
|
t1.setEvent(Event.of("PAY", "PAY"));
|
||||||
|
|
||||||
|
// Trigger point sends "PAYMENT" which contains "PAY". Under the old heuristics, this would falsely match!
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("PAYMENT").build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
// It should NOT match, because "PAYMENT" != "PAY" strictly.
|
||||||
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFilterCrossStateMachineRoutingByVertical() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
|
||||||
|
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.electronics.ElectronicsOrderController.post()",
|
||||||
|
"com.example.electronics.ElectronicsOrderService.processOrderEvent()"
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Testing against Electronics SM - should match
|
||||||
|
AnalysisResult resultElectronics = AnalysisResult.builder()
|
||||||
|
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(resultElectronics, null, null);
|
||||||
|
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
|
||||||
|
|
||||||
|
// Testing against ComputerStore SM - should NOT match because chain has Electronics service
|
||||||
|
AnalysisResult resultComputer = AnalysisResult.builder()
|
||||||
|
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration") // the non-electronics one
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(resultComputer, null, null);
|
||||||
|
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChainComp.getMatchedTransitions()).isNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAllowSharedServicesToRouteToMultipleStateMachines() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("PROCESSED", "PROCESSED")));
|
||||||
|
t1.setEvent(Event.of("PROCESS", "PROCESS"));
|
||||||
|
|
||||||
|
// Chain contains NO vertical prefix (e.g. CommonOrderService)
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("PROCESS").build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.CommonOrderController.post()",
|
||||||
|
"com.example.CommonOrderService.processOrderEvent()"
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult resultElectronics = AnalysisResult.builder()
|
||||||
|
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(resultElectronics, null, null);
|
||||||
|
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
|
||||||
|
|
||||||
|
// Should match Electronics because it's a shared service (no computerstore service in path)
|
||||||
|
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
|
||||||
|
|
||||||
|
AnalysisResult resultComputer = AnalysisResult.builder()
|
||||||
|
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(resultComputer, null, null);
|
||||||
|
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
|
||||||
|
|
||||||
|
// Should ALSO match ComputerStore because it's a shared service
|
||||||
|
assertThat(updatedChainComp.getMatchedTransitions()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFilterCrossStateMachineRoutingWithMultipleVerticals() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
|
||||||
|
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
|
||||||
|
|
||||||
|
// Chain 1: Electronics
|
||||||
|
CallChain chainElec = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of("com.example.electronics.ElectronicsOrderController.post()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Chain 2: Furniture
|
||||||
|
CallChain chainFurn = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of("com.example.furniture.FurnitureOrderController.post()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Chain 3: Groceries
|
||||||
|
CallChain chainGroc = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of("com.example.groceries.GroceriesOrderController.post()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 1: Electronics
|
||||||
|
AnalysisResult resElec = AnalysisResult.builder()
|
||||||
|
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 2: Furniture
|
||||||
|
AnalysisResult resFurn = AnalysisResult.builder()
|
||||||
|
.name("com.example.furniture.FurnitureOwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 3: Groceries
|
||||||
|
AnalysisResult resGroc = AnalysisResult.builder()
|
||||||
|
.name("com.example.groceries.GroceriesStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 4: Computer Store (The catch-all base one in computerstore package)
|
||||||
|
AnalysisResult resComp = AnalysisResult.builder()
|
||||||
|
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Test Electronics SM - Should only keep Electronics Chain
|
||||||
|
enricher.enrich(resElec, null, null);
|
||||||
|
assertThat(resElec.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1); // Elec
|
||||||
|
assertThat(resElec.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||||
|
assertThat(resElec.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||||
|
|
||||||
|
// Test Furniture SM - Should only keep Furniture Chain
|
||||||
|
enricher.enrich(resFurn, null, null);
|
||||||
|
assertThat(resFurn.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||||
|
assertThat(resFurn.getMetadata().getCallChains().get(1).getMatchedTransitions()).hasSize(1); // Furn
|
||||||
|
assertThat(resFurn.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||||
|
|
||||||
|
// Test Groceries SM - Should only keep Groceries Chain
|
||||||
|
enricher.enrich(resGroc, null, null);
|
||||||
|
assertThat(resGroc.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||||
|
assertThat(resGroc.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||||
|
assertThat(resGroc.getMetadata().getCallChains().get(2).getMatchedTransitions()).hasSize(1); // Groc
|
||||||
|
|
||||||
|
// Test ComputerStore SM - Should reject ALL because none belong to computerstore
|
||||||
|
enricher.enrich(resComp, null, null);
|
||||||
|
assertThat(resComp.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||||
|
assertThat(resComp.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||||
|
assertThat(resComp.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PropertyResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testYamlPropertiesAreLoadedAndFlattened(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path applicationYml = tempDir.resolve("application.yml");
|
||||||
|
Files.writeString(applicationYml, """
|
||||||
|
messaging:
|
||||||
|
sqs:
|
||||||
|
integration-system1:
|
||||||
|
callback:
|
||||||
|
queue: "integration-system1-queue"
|
||||||
|
""");
|
||||||
|
|
||||||
|
PropertyResolver resolver = new PropertyResolver();
|
||||||
|
Map<String, String> props = resolver.resolveProperties(tempDir);
|
||||||
|
|
||||||
|
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPropertiesFilesAreLoaded(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path applicationProps = tempDir.resolve("application.properties");
|
||||||
|
Files.writeString(applicationProps, "messaging.sqs.integration-system1.callback.queue=integration-system1-queue-props");
|
||||||
|
|
||||||
|
PropertyResolver resolver = new PropertyResolver();
|
||||||
|
Map<String, String> props = resolver.resolveProperties(tempDir);
|
||||||
|
|
||||||
|
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue-props");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPlaceholderResolution() {
|
||||||
|
PropertyResolver resolver = new PropertyResolver();
|
||||||
|
Map<String, String> properties = Map.of("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
|
||||||
|
|
||||||
|
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.integration-system1.callback.queue}", properties);
|
||||||
|
assertThat(resolved).isEqualTo("SQS: integration-system1-queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPlaceholderResolutionWithDefault() {
|
||||||
|
PropertyResolver resolver = new PropertyResolver();
|
||||||
|
Map<String, String> properties = Map.of();
|
||||||
|
|
||||||
|
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.queue:default-queue}", properties);
|
||||||
|
assertThat(resolved).isEqualTo("SQS: default-queue");
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,35 @@ public class GenericEventDetectorTest {
|
|||||||
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
|
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMessageBuilderIntermediateVariable(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("MyService.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"import org.springframework.messaging.support.MessageBuilder;\n" +
|
||||||
|
"import org.springframework.messaging.Message;\n" +
|
||||||
|
"import org.springframework.statemachine.StateMachine;\n" +
|
||||||
|
"public class MyService {\n" +
|
||||||
|
" private StateMachine<String, String> stateMachine;\n" +
|
||||||
|
" private void a(MyInterface myEvent) {\n" +
|
||||||
|
" MessageBuilder<MyInterface> messageBuilder = MessageBuilder.withPayload(myEvent).setHeader(\"k\", \"v\");\n" +
|
||||||
|
" stateMachine.sendEvent(messageBuilder.build());\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}\n");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
|
||||||
|
CompilationUnit cu = context.getCompilationUnits().iterator().next();
|
||||||
|
List<TriggerPoint> triggers = detector.detect(cu);
|
||||||
|
|
||||||
|
assertThat(triggers).hasSize(1);
|
||||||
|
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
|
||||||
|
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException {
|
void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException {
|
||||||
Path dir = tempDir.resolve("com/example");
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "PLACE_ORDER",
|
"event" : "PLACE_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||||
@@ -17,7 +19,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "CANCEL_ORDER",
|
"event" : "CANCEL_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||||
@@ -26,7 +30,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 21
|
"lineNumber" : 21,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "PAY_ORDER",
|
"event" : "PAY_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||||
@@ -35,7 +41,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "SHIP_ORDER",
|
"event" : "SHIP_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||||
@@ -44,7 +52,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "RETURN_ORDER",
|
"event" : "RETURN_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||||
@@ -53,7 +63,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -185,7 +197,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -219,7 +233,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 21
|
"lineNumber" : 21,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -248,10 +264,12 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ ]
|
"matchedTransitions" : null
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -278,7 +296,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -312,7 +332,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -346,7 +368,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "AUDIT_EVENT",
|
"event" : "AUDIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
||||||
@@ -17,16 +19,53 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "FALLBACK_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "PROFILED_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "PRIMARY_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
"event" : "EXTERNAL_TRIGGER",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
"methodName" : "processSubmit",
|
"methodName" : "processSubmit",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : "externalNotificationService",
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "SUBMIT_EVENT",
|
"event" : "SUBMIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -35,7 +74,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "CANCEL_EVENT",
|
"event" : "CANCEL_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -44,7 +85,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "[LIFECYCLE:RESTORE]",
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -53,7 +96,66 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "QUALIFIER_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "NAMED_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "AUTHORIZE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "processPayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 22,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "CAPTURE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 35,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : {
|
||||||
|
"paymentId" : "String"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : null,
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 28,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "REACTIVE_EVENT",
|
"event" : "REACTIVE_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||||
@@ -62,7 +164,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -137,6 +241,80 @@
|
|||||||
"interceptorType" : "Spring MVC Interceptor"
|
"interceptorType" : "Spring MVC Interceptor"
|
||||||
},
|
},
|
||||||
"parameters" : [ ]
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/primary",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testPrimary",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/primary",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/named",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testNamed",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/named",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/qualifier",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testQualifier",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/qualifier",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/fallback",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testFallback",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/fallback",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "GET /api/base/{id}",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "processBaseEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/base/{id}",
|
||||||
|
"verb" : "GET"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "capturePaymentEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/payment/{id}/capture",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"callChains" : [ {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
@@ -158,16 +336,14 @@
|
|||||||
"methodName" : "processSubmit",
|
"methodName" : "processSubmit",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : "externalNotificationService",
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : null
|
||||||
"sourceState" : "START",
|
|
||||||
"targetState" : "PROCESSING",
|
|
||||||
"event" : "EXTERNAL_TRIGGER"
|
|
||||||
} ]
|
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -190,7 +366,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -220,7 +398,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -250,7 +430,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -284,10 +466,12 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : "orderId",
|
"contextMachineId" : "orderId",
|
||||||
"matchedTransitions" : [ ]
|
"matchedTransitions" : null
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -314,7 +498,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -343,7 +529,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -351,6 +539,216 @@
|
|||||||
"targetState" : "START",
|
"targetState" : "START",
|
||||||
"event" : "AUDIT_EVENT"
|
"event" : "AUDIT_EVENT"
|
||||||
} ]
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/primary",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testPrimary",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/primary",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "PRIMARY_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/named",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testNamed",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/named",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "NAMED_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/qualifier",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testQualifier",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/qualifier",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "QUALIFIER_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/fallback",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testFallback",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/fallback",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "PRIMARY_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "GET /api/base/{id}",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "processBaseEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/base/{id}",
|
||||||
|
"verb" : "GET"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "AUTHORIZE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "processPayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 22,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "capturePaymentEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/payment/{id}/capture",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "CAPTURE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 35,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : {
|
||||||
|
"paymentId" : "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contextMachineId" : "paymentId",
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "capturePaymentEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/payment/{id}/capture",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : null,
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 28,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : "paymentId",
|
||||||
|
"matchedTransitions" : null
|
||||||
} ],
|
} ],
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"default" : {
|
"default" : {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "AUDIT_EVENT",
|
"event" : "AUDIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
||||||
@@ -17,16 +19,53 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "FALLBACK_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "PROFILED_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "PRIMARY_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
"event" : "EXTERNAL_TRIGGER",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
"methodName" : "processSubmit",
|
"methodName" : "processSubmit",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : "externalNotificationService",
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "SUBMIT_EVENT",
|
"event" : "SUBMIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -35,7 +74,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "CANCEL_EVENT",
|
"event" : "CANCEL_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -44,7 +85,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "[LIFECYCLE:RESTORE]",
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -53,7 +96,66 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "QUALIFIER_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "NAMED_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "AUTHORIZE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "processPayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 22,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
}, {
|
||||||
|
"event" : "CAPTURE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 35,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : {
|
||||||
|
"paymentId" : "String"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : null,
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 28,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "REACTIVE_EVENT",
|
"event" : "REACTIVE_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||||
@@ -62,7 +164,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -137,6 +241,80 @@
|
|||||||
"interceptorType" : "Spring MVC Interceptor"
|
"interceptorType" : "Spring MVC Interceptor"
|
||||||
},
|
},
|
||||||
"parameters" : [ ]
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/primary",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testPrimary",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/primary",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/named",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testNamed",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/named",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/qualifier",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testQualifier",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/qualifier",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/fallback",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testFallback",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/fallback",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "GET /api/base/{id}",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "processBaseEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/base/{id}",
|
||||||
|
"verb" : "GET"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "capturePaymentEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/payment/{id}/capture",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"callChains" : [ {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
@@ -158,16 +336,14 @@
|
|||||||
"methodName" : "processSubmit",
|
"methodName" : "processSubmit",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : "externalNotificationService",
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : null
|
||||||
"sourceState" : "START",
|
|
||||||
"targetState" : "PROCESSING",
|
|
||||||
"event" : "EXTERNAL_TRIGGER"
|
|
||||||
} ]
|
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -190,7 +366,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -220,7 +398,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -250,7 +430,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -284,10 +466,12 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : "orderId",
|
"contextMachineId" : "orderId",
|
||||||
"matchedTransitions" : [ ]
|
"matchedTransitions" : null
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -314,7 +498,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -343,7 +529,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -351,6 +539,216 @@
|
|||||||
"targetState" : "START",
|
"targetState" : "START",
|
||||||
"event" : "AUDIT_EVENT"
|
"event" : "AUDIT_EVENT"
|
||||||
} ]
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/primary",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testPrimary",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/primary",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "PRIMARY_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/named",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testNamed",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/named",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "NAMED_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/qualifier",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testQualifier",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/qualifier",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "QUALIFIER_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/quirk/fallback",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
||||||
|
"methodName" : "testFallback",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/quirk/fallback",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "PRIMARY_EVENT",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||||
|
"methodName" : "doQuirk",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "GET /api/base/{id}",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "processBaseEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/base/{id}",
|
||||||
|
"verb" : "GET"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "AUTHORIZE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "processPayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 22,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "capturePaymentEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/payment/{id}/capture",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "CAPTURE",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 35,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : {
|
||||||
|
"paymentId" : "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contextMachineId" : "paymentId",
|
||||||
|
"matchedTransitions" : null
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
|
||||||
|
"methodName" : "capturePaymentEndpoint",
|
||||||
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/payment/{id}/capture",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "id",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ "PathVariable" ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
|
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||||
|
"methodName" : "capturePayment",
|
||||||
|
"sourceFile" : null,
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 28,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
|
},
|
||||||
|
"contextMachineId" : "paymentId",
|
||||||
|
"matchedTransitions" : null
|
||||||
} ],
|
} ],
|
||||||
"properties" : {
|
"properties" : {
|
||||||
"default" : {
|
"default" : {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 15
|
"lineNumber" : 15,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -55,7 +57,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 15
|
"lineNumber" : 15,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 40
|
"lineNumber" : 40,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "ORDER_EVENT",
|
"event" : "ORDER_EVENT",
|
||||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
|
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
|
||||||
@@ -17,7 +19,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 52
|
"lineNumber" : 52,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -103,7 +107,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 40
|
"lineNumber" : 40,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -67,7 +69,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "eventProvider",
|
"event" : "eventProvider",
|
||||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
@@ -17,7 +19,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 50
|
"lineNumber" : 50,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
}, {
|
}, {
|
||||||
"event" : "customMessage",
|
"event" : "customMessage",
|
||||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
@@ -26,7 +30,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 78
|
"lineNumber" : 78,
|
||||||
|
"polymorphicEvents" : null,
|
||||||
|
"payloadTypes" : { }
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -140,7 +146,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ],
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -170,7 +178,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ],
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -200,7 +210,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ],
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -234,7 +246,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ],
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -268,7 +282,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 78
|
"lineNumber" : 78,
|
||||||
|
"polymorphicEvents" : [ ],
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -302,7 +318,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ],
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -336,7 +354,9 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 50
|
"lineNumber" : 50,
|
||||||
|
"polymorphicEvents" : [ ],
|
||||||
|
"payloadTypes" : { }
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ import java.util.List;
|
|||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// Enable diagnostic mode early before SLF4J initializes
|
||||||
|
for (String arg : args) {
|
||||||
|
if ("--debug".equals(arg)) {
|
||||||
|
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wiring including the specialized HTML exporter
|
// Wiring including the specialized HTML exporter
|
||||||
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter(), new HtmlExporter());
|
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter(), new HtmlExporter());
|
||||||
var exportService = new ExportService(exporters);
|
var exportService = new ExportService(exporters);
|
||||||
|
|||||||
@@ -59,11 +59,19 @@ public class HtmlExporterCommand implements Callable<Integer> {
|
|||||||
@Option(names = {"--no-metadata-pane"}, description = "Disable rendering the left metadata pane (entry points, flows) in the HTML viewer.")
|
@Option(names = {"--no-metadata-pane"}, description = "Disable rendering the left metadata pane (entry points, flows) in the HTML viewer.")
|
||||||
private boolean noMetadataPane;
|
private boolean noMetadataPane;
|
||||||
|
|
||||||
|
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
||||||
|
private boolean debug;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
var out = spec.commandLine().getOut();
|
var out = spec.commandLine().getOut();
|
||||||
var err = spec.commandLine().getErr();
|
var err = spec.commandLine().getErr();
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||||
|
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,cyan Diagnostic mode enabled|@"));
|
||||||
|
}
|
||||||
|
|
||||||
if (inputDir == null && jsonFile == null) {
|
if (inputDir == null && jsonFile == null) {
|
||||||
inputDir = Path.of(".");
|
inputDir = Path.of(".");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,10 +238,27 @@
|
|||||||
.tooltip-content {
|
.tooltip-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar for Tooltips */
|
||||||
|
.tooltip-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.tooltip-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.tooltip-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.tooltip-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
.payload-box {
|
.payload-box {
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
|
|||||||
@@ -81,13 +81,6 @@ public class HtmlExporterTest {
|
|||||||
|
|
||||||
// 7. Assertions - Surgical SVG Identification
|
// 7. Assertions - Surgical SVG Identification
|
||||||
// Every event should be wrapped in an identifying hyperlink for robust JS selection
|
// Every event should be wrapped in an identifying hyperlink for robust JS selection
|
||||||
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_html.html"), html);
|
|
||||||
|
|
||||||
click.kamil.springstatemachineexporter.exporter.ExportOptions plantUmlOptions = click.kamil.springstatemachineexporter.exporter.ExportOptions.builder()
|
|
||||||
.embedIdentifiers(true)
|
|
||||||
.build();
|
|
||||||
click.kamil.springstatemachineexporter.exporter.PlantUml pumlExporter = new click.kamil.springstatemachineexporter.exporter.PlantUml();
|
|
||||||
java.nio.file.Files.writeString(java.nio.file.Paths.get("debug_puml.puml"), pumlExporter.export(result, plantUmlOptions));
|
|
||||||
|
|
||||||
assertThat(html).contains("__CREATE");
|
assertThat(html).contains("__CREATE");
|
||||||
assertThat(html).contains("__ASSIGN");
|
assertThat(html).contains("__ASSIGN");
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.statemachine.config.EnableStateMachine;
|
||||||
|
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
||||||
|
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||||
|
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableStateMachine(name = "paymentStateMachine")
|
||||||
|
public class PaymentStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||||
|
states
|
||||||
|
.withStates()
|
||||||
|
.initial("NEW")
|
||||||
|
.state("AUTHORIZED")
|
||||||
|
.state("CAPTURED")
|
||||||
|
.state("DECLINED")
|
||||||
|
.state("QUIRK1")
|
||||||
|
.state("QUIRK2")
|
||||||
|
.state("QUIRK3")
|
||||||
|
.state("QUIRK4");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||||
|
transitions
|
||||||
|
.withExternal()
|
||||||
|
.source("NEW").target("AUTHORIZED")
|
||||||
|
.event("AUTHORIZE")
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source("AUTHORIZED").target("CAPTURED")
|
||||||
|
.event("CAPTURE")
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source("NEW").target("DECLINED")
|
||||||
|
.event("DECLINE")
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source("NEW").target("QUIRK1")
|
||||||
|
.event("PRIMARY_EVENT")
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source("NEW").target("QUIRK2")
|
||||||
|
.event("NAMED_EVENT")
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source("NEW").target("QUIRK3")
|
||||||
|
.event("QUALIFIER_EVENT")
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source("NEW").target("QUIRK4")
|
||||||
|
.event("FALLBACK_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class FallbackQuirkService implements QuirkService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("paymentStateMachine")
|
||||||
|
private StateMachine<String, String> stateMachine;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doQuirk() {
|
||||||
|
stateMachine.sendEvent("FALLBACK_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service("customName")
|
||||||
|
public class NamedQuirkService implements QuirkService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("paymentStateMachine")
|
||||||
|
private StateMachine<String, String> stateMachine;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doQuirk() {
|
||||||
|
stateMachine.sendEvent("NAMED_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
public interface PaymentService {
|
||||||
|
void processPayment(String paymentId);
|
||||||
|
void capturePayment(String paymentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.statemachine.persist.StateMachinePersister;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PaymentServiceImpl implements PaymentService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("paymentStateMachine")
|
||||||
|
private StateMachine<String, String> stateMachine;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StateMachinePersister<String, String, String> persister;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processPayment(String paymentId) {
|
||||||
|
// Send a trigger mapped strictly to PaymentStateMachineConfig
|
||||||
|
stateMachine.sendEvent("AUTHORIZE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void capturePayment(String paymentId) {
|
||||||
|
try {
|
||||||
|
persister.restore(stateMachine, paymentId);
|
||||||
|
} catch (Exception e) {}
|
||||||
|
|
||||||
|
org.springframework.messaging.Message<String> msg = org.springframework.messaging.support.MessageBuilder
|
||||||
|
.withPayload("CAPTURE")
|
||||||
|
.setHeader("paymentId", paymentId)
|
||||||
|
.build();
|
||||||
|
stateMachine.sendEvent(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Primary
|
||||||
|
public class PrimaryQuirkService implements QuirkService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("paymentStateMachine")
|
||||||
|
private StateMachine<String, String> stateMachine;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doQuirk() {
|
||||||
|
stateMachine.sendEvent("PRIMARY_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Profile("nonexistent")
|
||||||
|
public class ProfiledQuirkService implements QuirkService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("paymentStateMachine")
|
||||||
|
private StateMachine<String, String> stateMachine;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doQuirk() {
|
||||||
|
stateMachine.sendEvent("PROFILED_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QualifierQuirkService implements QuirkService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("paymentStateMachine")
|
||||||
|
private StateMachine<String, String> stateMachine;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doQuirk() {
|
||||||
|
stateMachine.sendEvent("QUALIFIER_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.service;
|
||||||
|
|
||||||
|
public interface QuirkService {
|
||||||
|
void doQuirk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.web;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
|
public interface BaseController {
|
||||||
|
|
||||||
|
@GetMapping("/api/base/{id}")
|
||||||
|
String processBaseEndpoint(@PathVariable String id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.web;
|
||||||
|
|
||||||
|
import click.kamil.examples.statemachine.extended.service.PaymentService;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class PaymentController implements BaseController {
|
||||||
|
|
||||||
|
private final PaymentService paymentService;
|
||||||
|
|
||||||
|
public PaymentController(PaymentService paymentService) {
|
||||||
|
this.paymentService = paymentService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String processBaseEndpoint(@PathVariable String id) {
|
||||||
|
paymentService.processPayment(id);
|
||||||
|
return "Started Base Payment: " + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/payment/{id}/capture")
|
||||||
|
public String capturePaymentEndpoint(@PathVariable String id) {
|
||||||
|
paymentService.capturePayment(id);
|
||||||
|
return "Captured: " + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package click.kamil.examples.statemachine.extended.web;
|
||||||
|
|
||||||
|
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class QuirkController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private QuirkService quirkService; // Should resolve to PrimaryQuirkService
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("customName")
|
||||||
|
private QuirkService someService; // Should resolve to NamedQuirkService
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("qualifierQuirkService")
|
||||||
|
private QuirkService anotherService; // Should resolve to QualifierQuirkService
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private QuirkService fallbackQuirkService; // Should resolve to FallbackQuirkService
|
||||||
|
|
||||||
|
@PostMapping("/api/quirk/primary")
|
||||||
|
public void testPrimary() {
|
||||||
|
quirkService.doQuirk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/quirk/named")
|
||||||
|
public void testNamed() {
|
||||||
|
someService.doQuirk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/quirk/qualifier")
|
||||||
|
public void testQualifier() {
|
||||||
|
anotherService.doQuirk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/quirk/fallback")
|
||||||
|
public void testFallback() {
|
||||||
|
fallbackQuirkService.doQuirk();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user