Compare commits
26 Commits
8d4ba0697e
...
ai-branch-
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f651e7dc9 | |||
| e06429cb02 | |||
| 9cf2d063b3 | |||
| 073df21395 | |||
| f9e6247eb3 | |||
| 76ac143370 | |||
| 9ca8b2fe37 | |||
| 56cdf96f2d | |||
| c37aa92c78 | |||
| 3ac057618f | |||
| d59f4ac166 | |||
| 19547d3c5f | |||
| ab37eb5d40 | |||
| bf9208d529 | |||
| 7077214c81 | |||
| 344e295106 | |||
| d6b1571f18 | |||
| e617fb1754 | |||
| 06dc0ba641 | |||
| ba412b905e | |||
| 198c5d830e | |||
| a383de5a5f | |||
| ef9ebe3512 | |||
| b480dc83ef | |||
| 0a23daae05 | |||
| e894566112 |
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);
|
||||||
|
|||||||
@@ -14,8 +14,16 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.matching.HeuristicEventMatchingEngine;
|
||||||
|
|
||||||
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
||||||
|
private final EventMatchingEngine matchingEngine = new HeuristicEventMatchingEngine();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
if (result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
|
if (result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
|
||||||
@@ -38,40 +46,46 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
List<MatchedTransition> matched = new ArrayList<>();
|
List<MatchedTransition> matched = new ArrayList<>();
|
||||||
|
|
||||||
for (Transition t : stateMachineTransitions) {
|
for (Transition t : stateMachineTransitions) {
|
||||||
if (t.getEvent() != null) {
|
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
||||||
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);
|
// Event matches
|
||||||
if (smEvent.equals(triggerEvent)) {
|
for (State smSourceState : t.getSourceStates()) {
|
||||||
// Event matches. Check source state if provided
|
|
||||||
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)) {
|
||||||
for (State smTargetState : t.getTargetStates()) {
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
String sourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
.sourceState(smSourceRaw)
|
||||||
matched.add(MatchedTransition.builder()
|
.targetState(smSourceRaw)
|
||||||
.sourceState(sourceRaw)
|
|
||||||
.targetState(targetRaw)
|
|
||||||
.event(smEventRaw)
|
.event(smEventRaw)
|
||||||
.build());
|
.build();
|
||||||
|
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||||
|
matched.add(mt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (State smTargetState : t.getTargetStates()) {
|
||||||
|
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
||||||
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
|
.sourceState(smSourceRaw)
|
||||||
|
.targetState(targetRaw)
|
||||||
|
.event(smEventRaw)
|
||||||
|
.build();
|
||||||
|
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||||
|
matched.add(mt);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!matched.isEmpty()) {
|
||||||
|
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
|
||||||
|
updatedChains.add(newChain);
|
||||||
|
} else {
|
||||||
|
updatedChains.add(chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new CallChain with the matched transitions
|
|
||||||
CallChain updatedChain = CallChain.builder()
|
|
||||||
.entryPoint(chain.getEntryPoint())
|
|
||||||
.methodChain(chain.getMethodChain())
|
|
||||||
.triggerPoint(chain.getTriggerPoint())
|
|
||||||
.contextMachineId(chain.getContextMachineId())
|
|
||||||
.matchedTransitions(matched)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
updatedChains.add(updatedChain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the metadata with the new call chains
|
// Update the metadata with the new call chains
|
||||||
@@ -85,12 +99,48 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
result.setMetadata(updatedMetadata);
|
result.setMetadata(updatedMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String simplify(String name) {
|
|
||||||
if (name == null) return "";
|
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||||
int dot = name.lastIndexOf('.');
|
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName);
|
||||||
if (dot >= 0) {
|
|
||||||
return name.substring(dot + 1);
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String simplify(String name) {
|
||||||
|
if (name == null) return null;
|
||||||
|
// Strip common suffixes
|
||||||
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
|
if (simplified.isEmpty()) {
|
||||||
|
simplified = name;
|
||||||
|
}
|
||||||
|
// Simplify full identifiers to just the last part (enum name)
|
||||||
|
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
|
||||||
|
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
|
|
||||||
|
public interface EventMatchingEngine {
|
||||||
|
/**
|
||||||
|
* Determines whether the given trigger point is a match for the state machine event.
|
||||||
|
*/
|
||||||
|
boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
|
||||||
|
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String triggerEvent = simplify(triggerPoint.getEvent());
|
||||||
|
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||||
|
String smEvent = simplify(smEventRaw);
|
||||||
|
|
||||||
|
boolean isWildcard = isWildcardVariable(triggerEvent);
|
||||||
|
|
||||||
|
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
||||||
|
|
||||||
|
boolean hasPolyMatch = false;
|
||||||
|
for (String pe : polyEvents) {
|
||||||
|
String simplePe = pe;
|
||||||
|
if (pe.contains(".")) {
|
||||||
|
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
|
||||||
|
hasPolyMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasPolyMatch || smEvent.equals(triggerEvent) || (polyEvents.isEmpty() && isWildcard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWildcardVariable(String eventStr) {
|
||||||
|
return eventStr.equals("event") || eventStr.equals("e") ||
|
||||||
|
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||||
|
eventStr.equals("payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String simplify(String name) {
|
||||||
|
if (name == null) return null;
|
||||||
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
|
if (simplified.isEmpty()) {
|
||||||
|
simplified = name;
|
||||||
|
}
|
||||||
|
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
|
||||||
|
public interface BeanResolutionEngine {
|
||||||
|
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||||
|
String targetVar = chain.getContextMachineId();
|
||||||
|
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||||
|
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||||
|
}
|
||||||
|
|
||||||
|
String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : "";
|
||||||
|
|
||||||
|
if (targetVar != null && !targetVar.isEmpty()) {
|
||||||
|
targetVar = targetVar.toLowerCase();
|
||||||
|
if (targetVar.endsWith("statemachine")) {
|
||||||
|
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
|
||||||
|
if (!prefix.isEmpty()) {
|
||||||
|
if (simplifiedMachineName.contains(prefix)) {
|
||||||
|
return true; // Explicit positive match
|
||||||
|
} else {
|
||||||
|
return false; // Explicit negative match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (simplifiedMachineName.contains(targetVar)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
|
||||||
|
String smPackage = getPackageName(currentMachineName);
|
||||||
|
String smSimple = getSimpleClassName(currentMachineName);
|
||||||
|
String smPrefix = getFirstCamelCaseWord(smSimple);
|
||||||
|
|
||||||
|
boolean hasPositiveMatch = false;
|
||||||
|
boolean hasStrongMismatch = false;
|
||||||
|
|
||||||
|
for (String method : chain.getMethodChain()) {
|
||||||
|
String chainClass = getClassNameOnly(method);
|
||||||
|
String chainPackage = getPackageName(chainClass);
|
||||||
|
String chainSimple = getSimpleClassName(chainClass);
|
||||||
|
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
||||||
|
|
||||||
|
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
|
||||||
|
hasPositiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
|
||||||
|
hasPositiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
|
||||||
|
hasStrongMismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPositiveMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasStrongMismatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
||||||
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
|
|
||||||
|
if (smPackage.startsWith(chainPackage) || chainPackage.startsWith(smPackage)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find deepest common package root
|
||||||
|
String[] p1 = smPackage.split("\\.");
|
||||||
|
String[] p2 = chainPackage.split("\\.");
|
||||||
|
int matchIdx = -1;
|
||||||
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
|
if (p1[i].equals(p2[i])) {
|
||||||
|
matchIdx = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the state machine's prefix is part of the shared common package,
|
||||||
|
// then the state machine represents the parent domain of the caller.
|
||||||
|
if (smPrefix != null && matchIdx >= 0) {
|
||||||
|
String lowerPrefix = smPrefix.toLowerCase();
|
||||||
|
for (int i = 0; i <= matchIdx; i++) {
|
||||||
|
if (p1[i].toLowerCase().contains(lowerPrefix) || lowerPrefix.contains(p1[i].toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
||||||
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
|
|
||||||
|
String[] p1 = smPackage.split("\\.");
|
||||||
|
String[] p2 = chainPackage.split("\\.");
|
||||||
|
|
||||||
|
int matchIdx = -1;
|
||||||
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
|
if (p1[i].equals(p2[i])) {
|
||||||
|
matchIdx = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> smDivergentTerms = new HashSet<>();
|
||||||
|
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
|
||||||
|
for (int i = matchIdx + 1; i < p1.length; i++) {
|
||||||
|
smDivergentTerms.add(p1[i].toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> chainDivergentTerms = new HashSet<>();
|
||||||
|
if (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
|
||||||
|
for (int i = matchIdx + 1; i < p2.length; i++) {
|
||||||
|
chainDivergentTerms.add(p2[i].toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smDivergentTerms.isEmpty() && !chainDivergentTerms.isEmpty()) {
|
||||||
|
Set<String> intersection = new HashSet<>(smDivergentTerms);
|
||||||
|
intersection.retainAll(chainDivergentTerms);
|
||||||
|
return intersection.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> extractDomainTerms(String pkg, String prefix) {
|
||||||
|
Set<String> terms = new HashSet<>();
|
||||||
|
if (pkg != null && !pkg.isEmpty()) {
|
||||||
|
String[] segments = pkg.split("\\.");
|
||||||
|
// Take segments from index 2 onwards if length > 2
|
||||||
|
int start = segments.length > 2 ? 2 : 0;
|
||||||
|
for (int i = start; i < segments.length; i++) {
|
||||||
|
terms.add(segments[i].toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prefix != null && !prefix.isEmpty()) {
|
||||||
|
terms.add(prefix.toLowerCase());
|
||||||
|
}
|
||||||
|
return terms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPackageName(String fqn) {
|
||||||
|
if (fqn == null || !fqn.contains(".")) return "";
|
||||||
|
return fqn.substring(0, fqn.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleClassName(String fqn) {
|
||||||
|
if (fqn == null) return "";
|
||||||
|
if (!fqn.contains(".")) return fqn;
|
||||||
|
return fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClassNameOnly(String methodFqn) {
|
||||||
|
if (methodFqn == null) return "";
|
||||||
|
String clean = methodFqn;
|
||||||
|
if (clean.contains("(")) {
|
||||||
|
clean = clean.substring(0, clean.indexOf('('));
|
||||||
|
}
|
||||||
|
if (clean.contains(".")) {
|
||||||
|
clean = clean.substring(0, clean.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFirstCamelCaseWord(String simpleName) {
|
||||||
|
if (simpleName == null || simpleName.isEmpty()) return null;
|
||||||
|
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
|
||||||
|
if (words.length > 0) {
|
||||||
|
return words[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CallEdge {
|
||||||
|
private final String targetMethod;
|
||||||
|
private final List<String> arguments;
|
||||||
|
}
|
||||||
@@ -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,5 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,12 +48,56 @@ 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 ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) {
|
||||||
|
String val = resolveInternal(mi.getExpression(), context, visited);
|
||||||
|
if (val != null) {
|
||||||
|
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
|
||||||
|
if (val.contains(".")) {
|
||||||
|
return val.substring(val.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for unresolved AST nodes
|
||||||
|
if (mi.getExpression() instanceof QualifiedName qn) {
|
||||||
|
return qn.getName().getIdentifier();
|
||||||
|
} else if (mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMethodBinding mb = mi.resolveMethodBinding();
|
||||||
|
if (mb != null) {
|
||||||
|
ITypeBinding returnType = mb.getReturnType();
|
||||||
|
if (returnType != null) {
|
||||||
|
java.util.List<String> values = context.getEnumValues(returnType.getQualifiedName());
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TypeDeclaration td = findEnclosingType(mi);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||||
|
if (md != null && md.getReturnType2() != null) {
|
||||||
|
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,278 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CallGraphBuilder {
|
|
||||||
private final CodebaseContext context;
|
|
||||||
private final ConstantResolver constantResolver;
|
|
||||||
private String currentMethodFqn;
|
|
||||||
private Map<String, List<CallEdge>> graph;
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
private static class CallEdge {
|
|
||||||
private final String targetMethod;
|
|
||||||
private final List<String> arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CallGraphBuilder(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
this.constantResolver = new ConstantResolver();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
|
||||||
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
|
||||||
List<CallChain> chains = new ArrayList<>();
|
|
||||||
|
|
||||||
for (EntryPoint ep : entryPoints) {
|
|
||||||
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
|
||||||
for (TriggerPoint tp : triggers) {
|
|
||||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
|
||||||
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
|
||||||
if (path != null) {
|
|
||||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
|
||||||
String contextMachineId = extractContextMachineId(path, callGraph);
|
|
||||||
chains.add(CallChain.builder()
|
|
||||||
.entryPoint(ep)
|
|
||||||
.triggerPoint(resolvedTp)
|
|
||||||
.methodChain(path)
|
|
||||||
.contextMachineId(contextMachineId)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chains;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
|
||||||
if (path.size() < 2) return tp;
|
|
||||||
|
|
||||||
String event = tp.getEvent();
|
|
||||||
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(tp.getClassName());
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, tp.getMethodName(), true);
|
|
||||||
if (md != null) {
|
|
||||||
int paramIndex = -1;
|
|
||||||
for (int i = 0; i < md.parameters().size(); i++) {
|
|
||||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
|
|
||||||
if (svd.getName().getIdentifier().equals(event)) {
|
|
||||||
paramIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paramIndex >= 0) {
|
|
||||||
String caller = path.get(path.size() - 2);
|
|
||||||
String target = path.get(path.size() - 1);
|
|
||||||
List<CallEdge> edges = callGraph.get(caller);
|
|
||||||
if (edges != null) {
|
|
||||||
for (CallEdge edge : edges) {
|
|
||||||
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
|
||||||
if (paramIndex < edge.getArguments().size()) {
|
|
||||||
String resolvedValue = edge.getArguments().get(paramIndex);
|
|
||||||
if (resolvedValue != null) {
|
|
||||||
return TriggerPoint.builder()
|
|
||||||
.event(resolvedValue)
|
|
||||||
.className(tp.getClassName())
|
|
||||||
.methodName(tp.getMethodName())
|
|
||||||
.sourceFile(tp.getSourceFile())
|
|
||||||
.lineNumber(tp.getLineNumber())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
|
||||||
for (String node : path) {
|
|
||||||
List<CallEdge> edges = callGraph.get(node);
|
|
||||||
if (edges != null) {
|
|
||||||
for (CallEdge edge : edges) {
|
|
||||||
String target = edge.getTargetMethod();
|
|
||||||
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
|
|
||||||
// Persister signatures usually like: restore(stateMachine, contextObj)
|
|
||||||
if (edge.getArguments().size() >= 2) {
|
|
||||||
return edge.getArguments().get(1); // The contextObj / machineId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, List<CallEdge>> buildCallGraph() {
|
|
||||||
graph = new HashMap<>();
|
|
||||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
|
||||||
cu.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodDeclaration node) {
|
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
|
||||||
if (td != null) {
|
|
||||||
currentMethodFqn = context.getFqn(td) + "." + node.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (currentMethodFqn != null) {
|
|
||||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
|
||||||
List<String> args = resolveArguments(node.arguments());
|
|
||||||
for (String calledMethod : calledMethods) {
|
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(SuperMethodInvocation node) {
|
|
||||||
if (currentMethodFqn != null) {
|
|
||||||
String methodName = node.getName().getIdentifier();
|
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
|
||||||
if (td != null) {
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
String calledMethod = null;
|
|
||||||
if (superTd != null) {
|
|
||||||
calledMethod = resolveMethodInType(superTd, methodName);
|
|
||||||
}
|
|
||||||
if (calledMethod == null) {
|
|
||||||
calledMethod = superFqn + "." + methodName;
|
|
||||||
}
|
|
||||||
List<String> args = resolveArguments(node.arguments());
|
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return graph;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveArguments(List<?> astArguments) {
|
|
||||||
List<String> args = new ArrayList<>();
|
|
||||||
for (Object argObj : astArguments) {
|
|
||||||
Expression expr = (Expression) argObj;
|
|
||||||
String val = constantResolver.resolve(expr, context);
|
|
||||||
if (val == null) {
|
|
||||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
|
||||||
}
|
|
||||||
args.add(val);
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
|
||||||
String baseCalled = resolveCalledMethod(node);
|
|
||||||
if (baseCalled == null) return Collections.emptyList();
|
|
||||||
|
|
||||||
List<String> allResolved = new ArrayList<>();
|
|
||||||
allResolved.add(baseCalled);
|
|
||||||
|
|
||||||
if (baseCalled.contains(".")) {
|
|
||||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
|
||||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(className);
|
|
||||||
for (String impl : impls) {
|
|
||||||
allResolved.add(impl + "." + methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allResolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveCalledMethod(MethodInvocation node) {
|
|
||||||
Expression receiver = node.getExpression();
|
|
||||||
String methodName = node.getName().getIdentifier();
|
|
||||||
|
|
||||||
if (receiver == null) {
|
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
|
||||||
if (td != null) {
|
|
||||||
return resolveMethodInType(td, methodName);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
|
||||||
if (binding != null) {
|
|
||||||
return binding.getQualifiedName() + "." + methodName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
String receiverName = sn.getIdentifier();
|
|
||||||
return receiverName + "." + methodName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (md != null) {
|
|
||||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
|
||||||
return context.getFqn(declaringTd) + "." + methodName;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
|
||||||
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
|
||||||
if (!visited.add(start)) return null; // Path-scoped cycle detection
|
|
||||||
|
|
||||||
List<CallEdge> neighbors = graph.get(start);
|
|
||||||
if (neighbors != null) {
|
|
||||||
for (CallEdge edge : neighbors) {
|
|
||||||
String neighbor = edge.getTargetMethod();
|
|
||||||
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
|
||||||
visited.remove(start);
|
|
||||||
return new ArrayList<>(List.of(start, target));
|
|
||||||
}
|
|
||||||
List<String> path = findPath(neighbor, target, graph, visited);
|
|
||||||
if (path != null) {
|
|
||||||
path.add(0, start);
|
|
||||||
visited.remove(start);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visited.remove(start);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isHeuristicMatch(String neighbor, String target) {
|
|
||||||
if (target.endsWith("." + neighbor)) return true;
|
|
||||||
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
|
||||||
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
|
||||||
return simpleNeighbor.equals(simpleTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
|
||||||
ASTNode parent = node.getParent();
|
|
||||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
|
||||||
parent = parent.getParent();
|
|
||||||
}
|
|
||||||
return (TypeDeclaration) parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CallGraphEngine {
|
||||||
|
List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class DynamicClasspathResolver {
|
||||||
|
|
||||||
|
public List<String> resolveClasspath(Path projectRoot) {
|
||||||
|
log.info("Attempting dynamic classpath resolution for project at {}", projectRoot);
|
||||||
|
if (projectRoot == null || !Files.exists(projectRoot)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||||
|
return resolveMaven(projectRoot);
|
||||||
|
} else if (Files.exists(projectRoot.resolve("build.gradle")) || Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||||
|
return resolveGradle(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("No Maven or Gradle configuration found at {}. Proceeding without dynamic classpath.", projectRoot);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> resolveMaven(Path projectRoot) {
|
||||||
|
log.info("Maven project detected. Resolving dependencies...");
|
||||||
|
Path cpFile = null;
|
||||||
|
try {
|
||||||
|
cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt");
|
||||||
|
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
|
||||||
|
if (mvnCmd == null) {
|
||||||
|
log.warn("Maven executable not found.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
mvnCmd,
|
||||||
|
"-q",
|
||||||
|
"dependency:build-classpath",
|
||||||
|
"-DincludeScope=compile",
|
||||||
|
"-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString()
|
||||||
|
);
|
||||||
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
|
runProcess(pb);
|
||||||
|
|
||||||
|
if (Files.exists(cpFile)) {
|
||||||
|
String cpString = Files.readString(cpFile).trim();
|
||||||
|
if (!cpString.isEmpty()) {
|
||||||
|
return Arrays.asList(cpString.split(File.pathSeparator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to dynamically resolve Maven classpath", e);
|
||||||
|
} finally {
|
||||||
|
if (cpFile != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(cpFile);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> resolveGradle(Path projectRoot) {
|
||||||
|
log.info("Gradle project detected. Resolving dependencies...");
|
||||||
|
Path initScript = null;
|
||||||
|
try {
|
||||||
|
initScript = Files.createTempFile("state_machine_exporter_init", ".gradle");
|
||||||
|
String gradleCmd = getCommand(projectRoot, "gradlew", "gradle");
|
||||||
|
if (gradleCmd == null) {
|
||||||
|
log.warn("Gradle executable not found.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String scriptContent = """
|
||||||
|
allprojects {
|
||||||
|
task smePrintClasspath {
|
||||||
|
doLast {
|
||||||
|
def cp = []
|
||||||
|
if (configurations.findByName("compileClasspath")) {
|
||||||
|
cp = configurations.compileClasspath.files
|
||||||
|
}
|
||||||
|
println "SME_CLASSPATH_MARKER:" + cp.join(File.pathSeparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(initScript, scriptContent);
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
gradleCmd,
|
||||||
|
"-q",
|
||||||
|
"-I", initScript.toAbsolutePath().toString(),
|
||||||
|
"smePrintClasspath"
|
||||||
|
);
|
||||||
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
|
String output = runProcessAndGetOutput(pb);
|
||||||
|
for (String line : output.split("\\r?\\n")) {
|
||||||
|
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
|
||||||
|
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
|
||||||
|
if (!cpString.isEmpty()) {
|
||||||
|
return Arrays.asList(cpString.split(File.pathSeparator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to dynamically resolve Gradle classpath", e);
|
||||||
|
} finally {
|
||||||
|
if (initScript != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(initScript);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCommand(Path projectRoot, String wrapper, String systemBinary) {
|
||||||
|
Path wrapperUnix = projectRoot.resolve(wrapper);
|
||||||
|
Path wrapperWin = projectRoot.resolve(wrapper + ".bat");
|
||||||
|
|
||||||
|
boolean isWin = System.getProperty("os.name").toLowerCase().contains("win");
|
||||||
|
|
||||||
|
if (isWin && Files.exists(wrapperWin)) return wrapperWin.toAbsolutePath().toString();
|
||||||
|
if (!isWin && Files.exists(wrapperUnix)) {
|
||||||
|
// Need absolute path like ./gradlew
|
||||||
|
return wrapperUnix.toAbsolutePath().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void runProcess(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||||
|
Process p = pb.start();
|
||||||
|
p.waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String runProcessAndGetOutput(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||||
|
Process p = pb.start();
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
out.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.waitFor();
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,10 +47,12 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
TriggerPoint trigger = buildTriggerPoint(node, cu, null);
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
||||||
if (trigger != null) {
|
if (builtTriggers != null) {
|
||||||
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
for (TriggerPoint trigger : builtTriggers) {
|
||||||
triggers.add(trigger);
|
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||||
|
triggers.add(trigger);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +64,23 @@ 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())) {
|
||||||
TriggerPoint trigger = buildTriggerPoint(node, cu, hint.getEvent());
|
String eventToUse = hint.getEvent();
|
||||||
if (trigger != null) {
|
if (eventToUse == null && hint.getEventArgumentIndex() != null) {
|
||||||
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
if (node.arguments().size() > hint.getEventArgumentIndex()) {
|
||||||
triggers.add(trigger);
|
Expression argExpr = (Expression) node.arguments().get(hint.getEventArgumentIndex());
|
||||||
|
eventToUse = argExpr.toString();
|
||||||
|
if (hint.getEventArgumentMethod() != null && !hint.getEventArgumentMethod().isEmpty()) {
|
||||||
|
eventToUse = eventToUse + "." + hint.getEventArgumentMethod() + "()";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, eventToUse);
|
||||||
|
if (builtTriggers != null) {
|
||||||
|
for (TriggerPoint trigger : builtTriggers) {
|
||||||
|
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||||
|
triggers.add(trigger);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,12 +117,12 @@ public class GenericEventDetector {
|
|||||||
return receiver.toString() + "." + methodName;
|
return receiver.toString() + "." + methodName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TriggerPoint buildTriggerPoint(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
private List<TriggerPoint> buildTriggerPoints(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
||||||
String eventValue = null;
|
String eventValue = null;
|
||||||
if (forcedEvent != null) {
|
if (forcedEvent != null) {
|
||||||
eventValue = context.resolveString(forcedEvent);
|
eventValue = context.resolveString(forcedEvent);
|
||||||
} else {
|
} else {
|
||||||
if (node.arguments().isEmpty()) return null;
|
if (node.arguments().isEmpty()) return Collections.emptyList();
|
||||||
Expression eventExpr = (Expression) node.arguments().get(0);
|
Expression eventExpr = (Expression) node.arguments().get(0);
|
||||||
|
|
||||||
// Try MessageBuilder extraction first
|
// Try MessageBuilder extraction first
|
||||||
@@ -123,55 +138,131 @@ public class GenericEventDetector {
|
|||||||
MethodDeclaration method = findEnclosingMethod(node);
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
TypeDeclaration type = findEnclosingType(node);
|
TypeDeclaration type = findEnclosingType(node);
|
||||||
|
|
||||||
if (type == null) return null;
|
if (type == null) return Collections.emptyList();
|
||||||
|
|
||||||
String sourceState = extractSourceState(node);
|
String sourceState = extractSourceState(node);
|
||||||
|
|
||||||
return TriggerPoint.builder()
|
List<TriggerPoint> results = new ArrayList<>();
|
||||||
.event(eventValue)
|
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||||
.sourceState(sourceState)
|
String[] parts = eventValue.substring(9).split(",");
|
||||||
.className(context.getFqn(type))
|
for (String part : parts) {
|
||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
if (!part.trim().isEmpty()) {
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
results.add(TriggerPoint.builder()
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.event(part.trim())
|
||||||
.build();
|
.sourceState(sourceState)
|
||||||
|
.className(context.getFqn(type))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(TriggerPoint.builder()
|
||||||
|
.event(eventValue)
|
||||||
|
.sourceState(sourceState)
|
||||||
|
.className(context.getFqn(type))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractSourceState(ASTNode node) {
|
private String extractSourceState(ASTNode node) {
|
||||||
ASTNode current = node.getParent();
|
ASTNode current = node;
|
||||||
while (current != null && !(current instanceof MethodDeclaration)) {
|
while (current != null && !(current instanceof MethodDeclaration)) {
|
||||||
if (current instanceof IfStatement ifStmt) {
|
ASTNode parent = current.getParent();
|
||||||
Expression expr = ifStmt.getExpression();
|
|
||||||
String state = extractStateFromExpression(expr);
|
// 1. Direct parent is IfStatement wrapper (e.g., if (state == X) { sm.sendEvent() })
|
||||||
if (state != null) return state;
|
if (parent instanceof IfStatement ifStmt) {
|
||||||
} else if (current instanceof SwitchCase switchCase) {
|
// Only consider if our current node is in the 'then' or 'else' part, not the condition
|
||||||
if (!switchCase.expressions().isEmpty()) {
|
if (ifStmt.getExpression() != current) {
|
||||||
return getSimpleNameString((Expression) switchCase.expressions().get(0));
|
Expression expr = ifStmt.getExpression();
|
||||||
|
String state = extractStateFromExpression(expr);
|
||||||
|
if (state != null) return state;
|
||||||
}
|
}
|
||||||
} else if (current instanceof SwitchStatement switchStmt) {
|
|
||||||
// If it's a switch block but we haven't hit a SwitchCase directly (AST hierarchy usually has SwitchCase as siblings of block statements)
|
|
||||||
// Actually, walking up from the node, we will hit the SwitchStatement, but we need the SwitchCase right before us in the block.
|
|
||||||
ASTNode parentBlock = current;
|
|
||||||
// It's complex to walk siblings. A simpler heuristic is to just use IfStatement and SwitchCase (if wrapped correctly).
|
|
||||||
}
|
}
|
||||||
current = current.getParent();
|
|
||||||
|
// 2. Parent is a Block or SwitchStatement: perform sibling traversal
|
||||||
|
if (parent instanceof Block block) {
|
||||||
|
String state = extractStateFromSiblings(current, block.statements());
|
||||||
|
if (state != null) return state;
|
||||||
|
} else if (parent instanceof SwitchStatement switchStmt) {
|
||||||
|
String state = extractStateFromSiblings(current, switchStmt.statements());
|
||||||
|
if (state != null) return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = parent;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractStateFromSiblings(ASTNode currentNode, List<?> statements) {
|
||||||
|
int index = statements.indexOf(currentNode);
|
||||||
|
if (index <= 0) return null; // No previous siblings
|
||||||
|
|
||||||
|
// Walk backwards through siblings
|
||||||
|
for (int i = index - 1; i >= 0; i--) {
|
||||||
|
Object stmtObj = statements.get(i);
|
||||||
|
|
||||||
|
if (stmtObj instanceof SwitchCase switchCase) {
|
||||||
|
if (!switchCase.expressions().isEmpty()) {
|
||||||
|
return getSimpleNameString((Expression) switchCase.expressions().get(0));
|
||||||
|
}
|
||||||
|
} else if (stmtObj instanceof IfStatement ifStmt) {
|
||||||
|
// Guard clause detection: check if the 'then' block has a return/throw
|
||||||
|
if (isGuardClause(ifStmt.getThenStatement())) {
|
||||||
|
Expression expr = ifStmt.getExpression();
|
||||||
|
// If it's a guard clause, it usually checks `state != EXPECTED`, so the true state *is* EXPECTED
|
||||||
|
String state = extractStateFromExpression(expr);
|
||||||
|
if (state != null) return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGuardClause(Statement thenStatement) {
|
||||||
|
if (thenStatement instanceof ReturnStatement || thenStatement instanceof ThrowStatement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (thenStatement instanceof Block block) {
|
||||||
|
for (Object obj : block.statements()) {
|
||||||
|
if (obj instanceof ReturnStatement || obj instanceof ThrowStatement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private String extractStateFromExpression(Expression expr) {
|
private String extractStateFromExpression(Expression expr) {
|
||||||
if (expr instanceof InfixExpression infix) {
|
if (expr instanceof InfixExpression infix) {
|
||||||
if (infix.getOperator() == InfixExpression.Operator.EQUALS) {
|
if (infix.getOperator() == InfixExpression.Operator.EQUALS || infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) {
|
||||||
Expression left = infix.getLeftOperand();
|
Expression left = infix.getLeftOperand();
|
||||||
Expression right = infix.getRightOperand();
|
Expression right = infix.getRightOperand();
|
||||||
// Usually one is a method call like getState(), the other is an enum
|
|
||||||
if (left instanceof QualifiedName || left instanceof SimpleName && !(right instanceof SimpleName)) {
|
// Ignore null checks entirely
|
||||||
|
if (left instanceof NullLiteral || right instanceof NullLiteral) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usually one is a method call like getState() or a variable like `state`
|
||||||
|
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
|
||||||
|
|
||||||
|
// If one is a QualifiedName (enum constant) or StringLiteral, it's likely the state
|
||||||
|
if (left instanceof QualifiedName || left instanceof StringLiteral || left instanceof FieldAccess) {
|
||||||
return getSimpleNameString(left);
|
return getSimpleNameString(left);
|
||||||
}
|
}
|
||||||
if (right instanceof QualifiedName || right instanceof SimpleName && !(left instanceof SimpleName)) {
|
if (right instanceof QualifiedName || right instanceof StringLiteral || right instanceof FieldAccess) {
|
||||||
return getSimpleNameString(right);
|
return getSimpleNameString(right);
|
||||||
}
|
}
|
||||||
return getSimpleNameString(right); // Fallback
|
|
||||||
|
// Fallback
|
||||||
|
return getSimpleNameString(right);
|
||||||
}
|
}
|
||||||
} else if (expr instanceof MethodInvocation mi) {
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
|
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
|
||||||
@@ -193,6 +284,10 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String extractEventFromMessageBuilder(Expression expr) {
|
private String extractEventFromMessageBuilder(Expression expr) {
|
||||||
|
if (expr instanceof CastExpression ce) {
|
||||||
|
return extractEventFromMessageBuilder(ce.getExpression());
|
||||||
|
}
|
||||||
|
|
||||||
if (expr instanceof SimpleName sn) {
|
if (expr instanceof SimpleName sn) {
|
||||||
String varName = sn.getIdentifier();
|
String varName = sn.getIdentifier();
|
||||||
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||||
@@ -218,6 +313,15 @@ public class GenericEventDetector {
|
|||||||
if (initializer[0] != null) {
|
if (initializer[0] != null) {
|
||||||
return extractEventFromMessageBuilder(initializer[0]); // recursive
|
return extractEventFromMessageBuilder(initializer[0]); // recursive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it has NO initializer, it might be a method parameter!
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return varName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,10 +344,23 @@ public class GenericEventDetector {
|
|||||||
if (resolved != null) return resolved;
|
if (resolved != null) return resolved;
|
||||||
|
|
||||||
// If not a constant, it might be a parameter like 'event'
|
// If not a constant, it might be a parameter like 'event'
|
||||||
|
if (payloadExpr instanceof CastExpression ce) {
|
||||||
|
payloadExpr = ce.getExpression();
|
||||||
|
}
|
||||||
|
|
||||||
if (payloadExpr instanceof SimpleName sn) {
|
if (payloadExpr instanceof SimpleName sn) {
|
||||||
return sn.getIdentifier(); // Return the parameter name so the call graph can fill it
|
String extracted = extractEventFromMessageBuilder(payloadExpr);
|
||||||
|
if (extracted != null) {
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
return sn.getIdentifier(); // Fall back to returning the parameter name
|
||||||
}
|
}
|
||||||
return payloadExpr.toString();
|
return payloadExpr.toString();
|
||||||
|
} else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
|
||||||
|
// If the event is obtained by calling a method on a provider/supplier parameter,
|
||||||
|
// return the provider's name so CallGraphBuilder can trace the lambda argument
|
||||||
|
String traced = extractEventFromMessageBuilder(sn);
|
||||||
|
return traced != null ? traced : sn.getIdentifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression receiver = current.getExpression();
|
Expression receiver = current.getExpression();
|
||||||
|
|||||||
@@ -0,0 +1,967 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class HeuristicCallGraphEngine implements CallGraphEngine {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private String currentMethodFqn;
|
||||||
|
private Map<String, List<CallEdge>> graph;
|
||||||
|
|
||||||
|
public HeuristicCallGraphEngine(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
this.constantResolver = new ConstantResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
|
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
||||||
|
List<CallChain> chains = new ArrayList<>();
|
||||||
|
|
||||||
|
for (EntryPoint ep : entryPoints) {
|
||||||
|
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
||||||
|
boolean foundAny = false;
|
||||||
|
for (TriggerPoint tp : triggers) {
|
||||||
|
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||||
|
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
||||||
|
if (path != null) {
|
||||||
|
foundAny = true;
|
||||||
|
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
||||||
|
String contextMachineId = extractContextMachineId(path, callGraph);
|
||||||
|
chains.add(CallChain.builder()
|
||||||
|
.entryPoint(ep)
|
||||||
|
.triggerPoint(resolvedTp)
|
||||||
|
.methodChain(path)
|
||||||
|
.contextMachineId(contextMachineId)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundAny && log.isDebugEnabled()) {
|
||||||
|
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chains;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
|
if (path.size() < 2) return tp;
|
||||||
|
|
||||||
|
String event = tp.getEvent();
|
||||||
|
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
|
||||||
|
|
||||||
|
String currentParamName = event;
|
||||||
|
String resolvedValue = event;
|
||||||
|
String methodSuffix = "";
|
||||||
|
|
||||||
|
// Extract method calls like .getType() so we can trace the base parameter
|
||||||
|
int dotIndex = currentParamName.indexOf('.');
|
||||||
|
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
|
||||||
|
char nextChar = currentParamName.charAt(dotIndex + 1);
|
||||||
|
if (Character.isLowerCase(nextChar)) {
|
||||||
|
methodSuffix = currentParamName.substring(dotIndex);
|
||||||
|
currentParamName = currentParamName.substring(0, dotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk backwards up the call chain
|
||||||
|
for (int i = path.size() - 1; i > 0; i--) {
|
||||||
|
String target = path.get(i);
|
||||||
|
String caller = path.get(i - 1);
|
||||||
|
|
||||||
|
// Find parameter index in target method
|
||||||
|
int paramIndex = getParameterIndex(target, currentParamName);
|
||||||
|
if (paramIndex < 0) {
|
||||||
|
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
|
||||||
|
String tracedVar = traceLocalVariable(target, currentParamName);
|
||||||
|
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||||
|
// Extract method calls like .getType() from the traced variable
|
||||||
|
int dotIdx = tracedVar.indexOf('.');
|
||||||
|
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||||
|
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||||
|
tracedVar = tracedVar.substring(0, dotIdx);
|
||||||
|
}
|
||||||
|
currentParamName = tracedVar;
|
||||||
|
resolvedValue = tracedVar + methodSuffix;
|
||||||
|
paramIndex = getParameterIndex(target, currentParamName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paramIndex < 0) {
|
||||||
|
break; // Parameter name changed or not found, stop tracing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the edge from caller to target to get the argument passed
|
||||||
|
List<CallEdge> edges = callGraph.get(caller);
|
||||||
|
boolean found = false;
|
||||||
|
if (edges != null) {
|
||||||
|
for (CallEdge edge : edges) {
|
||||||
|
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||||
|
if (paramIndex < edge.getArguments().size()) {
|
||||||
|
String arg = edge.getArguments().get(paramIndex);
|
||||||
|
if (arg != null) {
|
||||||
|
// If the argument passed has a method call, extract it
|
||||||
|
int dotIdx = arg.indexOf('.');
|
||||||
|
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
|
||||||
|
methodSuffix = arg.substring(dotIdx) + methodSuffix;
|
||||||
|
arg = arg.substring(0, dotIdx);
|
||||||
|
}
|
||||||
|
currentParamName = arg;
|
||||||
|
resolvedValue = arg + methodSuffix;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) break; // Could not map argument
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final check on the entry method
|
||||||
|
String entryMethod = path.get(0);
|
||||||
|
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
|
||||||
|
if (entryParamIndex < 0) {
|
||||||
|
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
|
||||||
|
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||||
|
int dotIdx = tracedVar.indexOf('.');
|
||||||
|
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||||
|
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||||
|
tracedVar = tracedVar.substring(0, dotIdx);
|
||||||
|
}
|
||||||
|
currentParamName = tracedVar;
|
||||||
|
resolvedValue = tracedVar + methodSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> polymorphicEvents = new ArrayList<>();
|
||||||
|
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
|
||||||
|
int lastDot = resolvedValue.lastIndexOf('.');
|
||||||
|
int firstDot = resolvedValue.indexOf('.');
|
||||||
|
int openParen = resolvedValue.indexOf('(', lastDot);
|
||||||
|
|
||||||
|
if (lastDot > 0 && openParen > lastDot) {
|
||||||
|
String varName = resolvedValue.substring(0, firstDot);
|
||||||
|
if (varName.contains("(")) {
|
||||||
|
varName = null;
|
||||||
|
}
|
||||||
|
String methodName = resolvedValue.substring(lastDot + 1, openParen);
|
||||||
|
|
||||||
|
String declaredType = null;
|
||||||
|
String sourceMethod = null;
|
||||||
|
|
||||||
|
if (varName != null) {
|
||||||
|
for (String methodFqn : path) {
|
||||||
|
declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||||
|
if (declaredType != null) {
|
||||||
|
sourceMethod = methodFqn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String baseExpr = resolvedValue.substring(0, lastDot);
|
||||||
|
if (baseExpr.contains("new ")) {
|
||||||
|
int newIdx = baseExpr.indexOf("new ");
|
||||||
|
int openParenBase = baseExpr.indexOf('(', newIdx);
|
||||||
|
if (openParenBase > newIdx) {
|
||||||
|
declaredType = baseExpr.substring(newIdx + 4, openParenBase).trim();
|
||||||
|
if (declaredType.contains("<")) {
|
||||||
|
declaredType = declaredType.substring(0, declaredType.indexOf('<'));
|
||||||
|
}
|
||||||
|
sourceMethod = "inline-instantiation";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (declaredType != null) {
|
||||||
|
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
|
||||||
|
List<String> typesToInspect = new ArrayList<>();
|
||||||
|
if ("inline-instantiation".equals(sourceMethod)) {
|
||||||
|
typesToInspect.add(declaredType);
|
||||||
|
} else {
|
||||||
|
typesToInspect.add(declaredType);
|
||||||
|
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||||
|
}
|
||||||
|
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
|
||||||
|
|
||||||
|
for (String type : typesToInspect) {
|
||||||
|
Set<String> visited = new HashSet<>();
|
||||||
|
TypeDeclaration baseTd = context.getTypeDeclaration(type);
|
||||||
|
CompilationUnit cuToUse = null;
|
||||||
|
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
|
||||||
|
cuToUse = (CompilationUnit) baseTd.getRoot();
|
||||||
|
} else {
|
||||||
|
String firstPathMethod = path.get(0);
|
||||||
|
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
|
||||||
|
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
|
||||||
|
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
|
||||||
|
cuToUse = (CompilationUnit) entryTd.getRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
|
||||||
|
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
|
||||||
|
for (String constant : constants) {
|
||||||
|
if (!polymorphicEvents.contains(constant)) {
|
||||||
|
polymorphicEvents.add(constant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant
|
||||||
|
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
|
||||||
|
// for string literals or enum-like constants.
|
||||||
|
boolean hasValidConstant = false;
|
||||||
|
for (String ev : polymorphicEvents) {
|
||||||
|
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
|
||||||
|
if (val.equals(val.toUpperCase()) && val.length() > 2) {
|
||||||
|
hasValidConstant = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidConstant && resolvedValue.contains("(")) {
|
||||||
|
java.util.List<String> scraped = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
// Extract "STRING_LITERALS"
|
||||||
|
java.util.regex.Matcher m1 = java.util.regex.Pattern.compile("\"([^\"]+)\"").matcher(resolvedValue);
|
||||||
|
while (m1.find()) {
|
||||||
|
scraped.add(m1.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract ENUM_LIKE_CONSTANTS
|
||||||
|
java.util.regex.Matcher m2 = java.util.regex.Pattern.compile("\\b([A-Z_]{3,})\\b").matcher(resolvedValue);
|
||||||
|
while (m2.find()) {
|
||||||
|
if (!scraped.contains(m2.group(1))) {
|
||||||
|
scraped.add(m2.group(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scraped.isEmpty()) {
|
||||||
|
polymorphicEvents.clear();
|
||||||
|
polymorphicEvents.addAll(scraped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones
|
||||||
|
if (polymorphicEvents.size() > 1) {
|
||||||
|
polymorphicEvents.removeIf(e -> {
|
||||||
|
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
|
||||||
|
return !val.equals(val.toUpperCase()) || val.length() <= 2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
||||||
|
return TriggerPoint.builder()
|
||||||
|
.event(resolvedValue)
|
||||||
|
.className(tp.getClassName())
|
||||||
|
.methodName(tp.getMethodName())
|
||||||
|
.sourceFile(tp.getSourceFile())
|
||||||
|
.lineNumber(tp.getLineNumber())
|
||||||
|
.polymorphicEvents(polymorphicEvents)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return tp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getParameterIndex(String methodFqn, String paramName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null) {
|
||||||
|
for (int i = 0; i < md.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(paramName)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getVariableDeclaredType(String methodFqn, String varName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null) {
|
||||||
|
for (Object pObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return svd.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String[] foundType = new String[1];
|
||||||
|
if (md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
foundType[0] = node.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return foundType[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||||
|
if (depth > 20) return Collections.emptyList();
|
||||||
|
String fqn = className + "." + methodName;
|
||||||
|
if (!visited.add(fqn)) return Collections.emptyList();
|
||||||
|
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||||
|
if (tempTd == null) {
|
||||||
|
tempTd = context.getTypeDeclaration(className);
|
||||||
|
}
|
||||||
|
final TypeDeclaration td = tempTd;
|
||||||
|
if (td == null) {
|
||||||
|
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
|
||||||
|
}
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
Expression retExpr = node.getExpression();
|
||||||
|
if (retExpr != null) {
|
||||||
|
boolean handled = false;
|
||||||
|
if (retExpr instanceof MethodInvocation mi) {
|
||||||
|
// Follow delegation first
|
||||||
|
String called = resolveCalledMethod(mi);
|
||||||
|
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
|
||||||
|
if (called != null && called.contains(".")) {
|
||||||
|
if (visited.contains(called)) {
|
||||||
|
handled = true;
|
||||||
|
} else {
|
||||||
|
String cName = called.substring(0, called.lastIndexOf('.'));
|
||||||
|
String mName = called.substring(called.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||||
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
String val = constantResolver.resolve(retExpr, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
constants.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
constants.add(val);
|
||||||
|
}
|
||||||
|
} else if (retExpr instanceof QualifiedName qn) {
|
||||||
|
constants.add(qn.toString());
|
||||||
|
} else if (retExpr instanceof SimpleName sn) {
|
||||||
|
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
constants.addAll(consts);
|
||||||
|
} else {
|
||||||
|
constants.add(sn.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(fqn);
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String traceLocalVariable(String methodFqn, String varName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
final Expression[] initializer = new Expression[1];
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
initializer[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initializer[0] != null) {
|
||||||
|
Expression expr = traceVariable(initializer[0]);
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
|
||||||
|
Expression innerMost = unwrapMethodInvocation(mi, 0);
|
||||||
|
if (innerMost instanceof MethodInvocation innerMi) {
|
||||||
|
if (innerMi.getExpression() instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
|
||||||
|
}
|
||||||
|
return innerMi.getName().getIdentifier() + "()";
|
||||||
|
}
|
||||||
|
if (innerMost instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return innerMost.toString();
|
||||||
|
}
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return expr.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
||||||
|
if (depth > 5) return mi;
|
||||||
|
if (!mi.arguments().isEmpty()) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
|
for (String node : path) {
|
||||||
|
List<CallEdge> edges = callGraph.get(node);
|
||||||
|
if (edges != null) {
|
||||||
|
for (CallEdge edge : edges) {
|
||||||
|
String target = edge.getTargetMethod();
|
||||||
|
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
|
||||||
|
// Persister signatures usually like: restore(stateMachine, contextObj)
|
||||||
|
if (edge.getArguments().size() >= 2) {
|
||||||
|
return edge.getArguments().get(1); // The contextObj / machineId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (MethodDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<CallEdge>> buildCallGraph() {
|
||||||
|
graph = new HashMap<>();
|
||||||
|
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
MethodDeclaration md = findEnclosingMethod(node);
|
||||||
|
if (md != null) {
|
||||||
|
TypeDeclaration td = findEnclosingType(md);
|
||||||
|
if (td != null) {
|
||||||
|
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||||
|
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||||
|
List<String> args = resolveArguments(node.arguments());
|
||||||
|
for (String calledMethod : calledMethods) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||||
|
}
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
|
String typeName = emr.getExpression().toString();
|
||||||
|
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||||
|
TypeDeclaration td2 = findEnclosingType(node);
|
||||||
|
if (td2 != null) {
|
||||||
|
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||||
|
if (refMethod != null) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String fallbackTypeFqn = null;
|
||||||
|
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
fallbackTypeFqn = binding.getQualifiedName();
|
||||||
|
} else if (emr.getExpression() instanceof SimpleName sn) {
|
||||||
|
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||||
|
}
|
||||||
|
if (fallbackTypeFqn != null) {
|
||||||
|
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
|
for (String impl : impls) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperMethodInvocation node) {
|
||||||
|
MethodDeclaration md = findEnclosingMethod(node);
|
||||||
|
if (md != null) {
|
||||||
|
TypeDeclaration tdOuter = findEnclosingType(md);
|
||||||
|
if (tdOuter != null) {
|
||||||
|
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
String calledMethod = null;
|
||||||
|
if (superTd != null) {
|
||||||
|
calledMethod = resolveMethodInType(superTd, methodName);
|
||||||
|
}
|
||||||
|
if (calledMethod == null) {
|
||||||
|
calledMethod = superFqn + "." + methodName;
|
||||||
|
}
|
||||||
|
List<String> args = resolveArguments(node.arguments());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveArguments(List<?> astArguments) {
|
||||||
|
List<String> args = new ArrayList<>();
|
||||||
|
for (Object argObj : astArguments) {
|
||||||
|
Expression expr = (Expression) argObj;
|
||||||
|
|
||||||
|
// Extract from lambda
|
||||||
|
if (expr instanceof LambdaExpression le) {
|
||||||
|
ASTNode body = le.getBody();
|
||||||
|
if (body instanceof Expression bodyExpr) {
|
||||||
|
expr = bodyExpr;
|
||||||
|
} else if (body instanceof Block block) {
|
||||||
|
for (Object stmtObj : block.statements()) {
|
||||||
|
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
||||||
|
expr = rs.getExpression();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expr instanceof ExpressionMethodReference emr) {
|
||||||
|
TypeDeclaration td = findEnclosingType(expr);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
for (Object stmtObj : md.getBody().statements()) {
|
||||||
|
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
||||||
|
expr = rs.getExpression();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||||
|
Expression tracedExpr = traceVariable(expr);
|
||||||
|
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
||||||
|
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
|
||||||
|
Expression firstArg = (Expression) cic.arguments().get(0);
|
||||||
|
String resolved = constantResolver.resolve(firstArg, context);
|
||||||
|
if (resolved != null) {
|
||||||
|
expr = firstArg; // Only unwrap if it's actually a constant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String val = constantResolver.resolve(expr, context);
|
||||||
|
if (val == null) {
|
||||||
|
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
||||||
|
}
|
||||||
|
args.add(val);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||||
|
String baseCalled = resolveCalledMethod(node);
|
||||||
|
if (baseCalled == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
List<String> allResolved = new ArrayList<>();
|
||||||
|
allResolved.add(baseCalled);
|
||||||
|
|
||||||
|
if (baseCalled.contains(".")) {
|
||||||
|
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||||
|
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(className);
|
||||||
|
for (String impl : impls) {
|
||||||
|
allResolved.add(impl + "." + methodName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCalledMethod(MethodInvocation node) {
|
||||||
|
Expression receiver = node.getExpression();
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
|
||||||
|
if (receiver == null) {
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
return resolveMethodInType(td, methodName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
return binding.getQualifiedName() + "." + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiver instanceof ThisExpression) {
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td) + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||||
|
if (fallbackTypeFqn != null) {
|
||||||
|
return fallbackTypeFqn + "." + methodName;
|
||||||
|
}
|
||||||
|
String receiverName = sn.getIdentifier();
|
||||||
|
return receiverName + "." + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||||
|
String varName = receiverNameNode.getIdentifier();
|
||||||
|
|
||||||
|
// 1. Check local variables in enclosing method
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
// Check parameters
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return resolveTypeToFqn(svd.getType(), receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check method body (local variables)
|
||||||
|
if (enclosingMethod.getBody() != null) {
|
||||||
|
Type[] foundType = new Type[1];
|
||||||
|
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
foundType[0] = node.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundType[0] != null) {
|
||||||
|
return resolveTypeToFqn(foundType[0], receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check fields in enclosing class
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||||
|
if (enclosingType != null) {
|
||||||
|
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||||
|
for (Object fragObj : field.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null) {
|
||||||
|
TypeDeclaration declaringTd = findEnclosingType(md);
|
||||||
|
return context.getFqn(declaringTd) + "." + methodName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||||
|
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||||
|
if (!visited.add(start)) return null; // Path-scoped cycle detection
|
||||||
|
|
||||||
|
List<CallEdge> neighbors = graph.get(start);
|
||||||
|
if (neighbors != null) {
|
||||||
|
for (CallEdge edge : neighbors) {
|
||||||
|
String neighbor = edge.getTargetMethod();
|
||||||
|
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||||
|
visited.remove(start);
|
||||||
|
return new ArrayList<>(List.of(start, target));
|
||||||
|
}
|
||||||
|
List<String> path = findPath(neighbor, target, graph, visited);
|
||||||
|
if (path != null) {
|
||||||
|
path.add(0, start);
|
||||||
|
visited.remove(start);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Path search dead-end at {} when looking for {}", start, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.remove(start);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||||
|
if (target.endsWith("." + neighbor)) return true;
|
||||||
|
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
||||||
|
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
||||||
|
return simpleNeighbor.equals(simpleTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
private Expression traceVariable(Expression expr) {
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
final Expression[] initializer = new Expression[1];
|
||||||
|
enclosingMethod.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
initializer[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (initializer[0] != null) {
|
||||||
|
return traceVariable(initializer[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. Check Field Declarations
|
||||||
|
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
|
||||||
|
Expression right = frag.getInitializer();
|
||||||
|
if (right instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(right, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.ASTVisitor assignmentVisitor = new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression left = node.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
|
||||||
|
|
||||||
|
Expression right = node.getRightHandSide();
|
||||||
|
if (right instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
|
||||||
|
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
|
||||||
|
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
|
||||||
|
} else if (mi.getExpression() instanceof SimpleName) {
|
||||||
|
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(right, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation node) {
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
Expression arg = (Expression) argObj;
|
||||||
|
String val = constantResolver.resolve(arg, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation node) {
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
Expression arg = (Expression) argObj;
|
||||||
|
String val = constantResolver.resolve(arg, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Check Constructors
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.isConstructor() && md.getBody() != null) {
|
||||||
|
md.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Initializers
|
||||||
|
for (Object bodyDecl : td.bodyDeclarations()) {
|
||||||
|
if (bodyDecl instanceof org.eclipse.jdt.core.dom.Initializer init && init.getBody() != null) {
|
||||||
|
init.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -21,9 +21,10 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
private final GenericEventDetector eventDetector;
|
private final GenericEventDetector eventDetector;
|
||||||
private final SpringMvcDetector mvcDetector;
|
private final SpringMvcDetector mvcDetector;
|
||||||
private final MessagingDetector messagingDetector;
|
private final MessagingDetector messagingDetector;
|
||||||
private final CallGraphBuilder callGraphBuilder;
|
private final CallGraphEngine callGraphEngine;
|
||||||
private final LifecycleDetector lifecycleDetector;
|
private final LifecycleDetector lifecycleDetector;
|
||||||
private final InterceptorDetector interceptorDetector;
|
private final InterceptorDetector interceptorDetector;
|
||||||
|
private final SpringComponentDetector componentDetector;
|
||||||
|
|
||||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@@ -31,9 +32,10 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
||||||
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
||||||
this.messagingDetector = new MessagingDetector(context);
|
this.messagingDetector = new MessagingDetector(context);
|
||||||
this.callGraphBuilder = new CallGraphBuilder(context);
|
this.callGraphEngine = new HeuristicCallGraphEngine(context);
|
||||||
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 +60,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;
|
||||||
@@ -66,7 +69,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
@Override
|
@Override
|
||||||
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
|
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
|
||||||
return callGraphBuilder.findChains(entryPoints, triggers);
|
return callGraphEngine.findChains(entryPoints, triggers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public class MessagingDetector {
|
|||||||
.name(protocolName + ": " + destination)
|
.name(protocolName + ": " + destination)
|
||||||
.className(context.getFqn((TypeDeclaration) method.getParent()))
|
.className(context.getFqn((TypeDeclaration) method.getParent()))
|
||||||
.methodName(method.getName().getIdentifier())
|
.methodName(method.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn((TypeDeclaration) method.getParent())))
|
||||||
.metadata(metadata)
|
.metadata(metadata)
|
||||||
.parameters(extractParameters(method))
|
.parameters(extractParameters(method))
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -240,7 +240,7 @@ public class AstTransitionParser {
|
|||||||
t.setEvent(Event.of(QuotedExpression.of(resolved).toStringWithoutQuotes()));
|
t.setEvent(Event.of(QuotedExpression.of(resolved).toStringWithoutQuotes()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "guard" -> {
|
case "guard", "guardExpression" -> {
|
||||||
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
parseGuard(resolved, t, cu, context);
|
parseGuard(resolved, t, cu, context);
|
||||||
}
|
}
|
||||||
@@ -248,6 +248,10 @@ public class AstTransitionParser {
|
|||||||
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
parseAction(resolved, t, cu, context);
|
parseAction(resolved, t, cu, context);
|
||||||
}
|
}
|
||||||
|
case "timer", "timerOnce" -> {
|
||||||
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
|
t.setEvent(Event.of(methodName + "(" + resolved.toString() + ")", methodName + "(" + resolved.toString() + ")"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ public class CodebaseContext {
|
|||||||
private final List<CompilationUnit> allCus = new ArrayList<>();
|
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||||
|
|
||||||
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
||||||
|
private final Map<String, List<String>> enumValues = new HashMap<>();
|
||||||
|
|
||||||
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
|
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
|
||||||
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
|
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
|
||||||
@@ -188,6 +189,8 @@ public class CodebaseContext {
|
|||||||
for (Object type : cu.types()) {
|
for (Object type : cu.types()) {
|
||||||
if (type instanceof TypeDeclaration td) {
|
if (type instanceof TypeDeclaration td) {
|
||||||
indexType(td, packageName, javaFile);
|
indexType(td, packageName, javaFile);
|
||||||
|
} else if (type instanceof EnumDeclaration ed) {
|
||||||
|
indexEnum(ed, packageName, javaFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,9 +213,18 @@ 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) {
|
||||||
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
for (Object itfObj : td.superInterfaceTypes()) {
|
||||||
|
Type itf = (Type) itfObj;
|
||||||
|
String itfName = extractTypeName(itf);
|
||||||
|
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type superclass = td.getSuperclassType();
|
||||||
|
if (superclass != null) {
|
||||||
|
String superName = extractTypeName(superclass);
|
||||||
|
interfaceToImpls.computeIfAbsent(superName, k -> new ArrayList<>()).add(fqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively index nested types
|
// Recursively index nested types
|
||||||
@@ -224,15 +236,67 @@ 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) return interfaceToImpls.getOrDefault(fqn, Collections.emptyList());
|
String fqn = simpleNameToFqn.get(typeName);
|
||||||
|
if (fqn != null) {
|
||||||
|
directImpls = interfaceToImpls.get(fqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Collections.emptyList();
|
// Try simple name match if input was FQN
|
||||||
|
if (directImpls == null && typeName.contains(".")) {
|
||||||
|
String simpleName = typeName.substring(typeName.lastIndexOf('.') + 1);
|
||||||
|
directImpls = interfaceToImpls.get(simpleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directImpls != null) {
|
||||||
|
for (String impl : directImpls) {
|
||||||
|
results.add(impl);
|
||||||
|
// Recursively find implementations of the implementation (e.g., subclasses of an abstract class)
|
||||||
|
collectImplementations(impl, results, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
|
||||||
|
String simpleName = ed.getName().getIdentifier();
|
||||||
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
|
classes.put(fqn, (CompilationUnit) ed.getRoot());
|
||||||
|
classPaths.put(fqn, javaFile);
|
||||||
|
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
for (Object o : ed.enumConstants()) {
|
||||||
|
if (o instanceof EnumConstantDeclaration ecd) {
|
||||||
|
values.add(fqn + "." + ecd.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enumValues.put(fqn, values);
|
||||||
|
if (!simpleNameToFqn.containsKey(simpleName)) {
|
||||||
|
simpleNameToFqn.put(simpleName, fqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getEnumValues(String fqnOrSimpleName) {
|
||||||
|
if (fqnOrSimpleName == null) return null;
|
||||||
|
List<String> values = enumValues.get(fqnOrSimpleName);
|
||||||
|
if (values != null) return values;
|
||||||
|
String fqn = simpleNameToFqn.get(fqnOrSimpleName);
|
||||||
|
if (fqn != null) return enumValues.get(fqn);
|
||||||
|
// Also try stripping array or generic types if any (e.g. MyEnum[])
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public State resolveState(Expression expr, CompilationUnit cu) {
|
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ public class FileUtils {
|
|||||||
return paths
|
return paths
|
||||||
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
|
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
|
||||||
.filter(p -> matchers.stream().noneMatch(m -> m.matches(p)))
|
.filter(p -> matchers.stream().noneMatch(m -> m.matches(p)))
|
||||||
|
.map(Path::toAbsolutePath)
|
||||||
|
.map(Path::normalize)
|
||||||
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,11 +54,22 @@ 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;
|
||||||
|
|
||||||
|
@Option(names = {"--resolve-classpath"}, description = "Automatically run Maven/Gradle to resolve full classpath dependencies for perfect AST bindings.", defaultValue = "false")
|
||||||
|
private boolean resolveClasspath;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
var out = spec.commandLine().getOut();
|
var out = spec.commandLine().getOut();
|
||||||
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(".");
|
||||||
}
|
}
|
||||||
@@ -79,7 +90,7 @@ public class ExporterCommand implements Callable<Integer> {
|
|||||||
if (jsonFile != null) {
|
if (jsonFile != null) {
|
||||||
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
|
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
|
||||||
} else {
|
} else {
|
||||||
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, eventFormat, stateFormat);
|
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, resolveClasspath);
|
||||||
}
|
}
|
||||||
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -116,23 +116,23 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
|
|
||||||
sb.append(" <<").append(styleClass).append(">>");
|
sb.append(" <<").append(styleClass).append(">>");
|
||||||
|
|
||||||
if (t.getEvent() != null) {
|
|
||||||
String eventStr = options.formatEvent(t.getEvent());
|
|
||||||
if (eventStr != null && !eventStr.isBlank()) {
|
|
||||||
sb.append(" <<e_").append(normalize(eventStr)).append(">>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String label = buildLabel(t, options);
|
String label = buildLabel(t, options);
|
||||||
if (options.isEmbedIdentifiers()) {
|
if (options.isEmbedIdentifiers()) {
|
||||||
String eventStr = t.getEvent() != null ? options.formatEvent(t.getEvent()) : null;
|
String eventStr = t.getEvent() != null ? options.formatEvent(t.getEvent()) : null;
|
||||||
String linkId = eventStr != null && !eventStr.isBlank() ? "link_" + normalize(source) + "__" + normalize(eventStr) : "link_anon_" + normalize(source) + "_" + normalize(target);
|
String linkId = eventStr != null && !eventStr.isBlank() ? "link_" + normalize(source) + "__" + normalize(eventStr) : "link_anon_" + normalize(source) + "_" + normalize(target);
|
||||||
// Force a label even if empty to ensure the link group exists in SVG
|
|
||||||
String displayLabel = label.isEmpty() ? " " : label;
|
sb.append(" : [[#").append(linkId).append(" ");
|
||||||
// Brackets [...] in the label break PlantUML link syntax [[url label]],
|
if (!label.isEmpty()) {
|
||||||
// so we strip them for the interactive identification label.
|
String sanitized = label.replace("[", "[")
|
||||||
String safeLabel = displayLabel.replaceAll("[\\[\\]]", "");
|
.replace("]", "]")
|
||||||
sb.append(" : ").append("[[#").append(linkId).append(" ").append(safeLabel).append("]]");
|
.replace("<", "<")
|
||||||
|
.replace(">", ">");
|
||||||
|
sb.append(sanitized);
|
||||||
|
} else {
|
||||||
|
sb.append(".");
|
||||||
|
}
|
||||||
|
sb.append("]]");
|
||||||
} else if (!label.isEmpty()) {
|
} else if (!label.isEmpty()) {
|
||||||
sb.append(" : ").append(label);
|
sb.append(" : ").append(label);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,22 +53,22 @@ public class ExportService {
|
|||||||
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
||||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat);
|
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean resolveClasspath) throws IOException {
|
||||||
CodebaseContext context = new CodebaseContext();
|
CodebaseContext context = new CodebaseContext();
|
||||||
context.setActiveProfiles(activeProfiles);
|
context.setActiveProfiles(activeProfiles);
|
||||||
|
|
||||||
@@ -94,6 +94,18 @@ public class ExportService {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
log.info("Setting JDT sourcepath: {}", sourcepaths);
|
log.info("Setting JDT sourcepath: {}", sourcepaths);
|
||||||
context.setSourcepath(sourcepaths);
|
context.setSourcepath(sourcepaths);
|
||||||
|
|
||||||
|
if (resolveClasspath) {
|
||||||
|
click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver classpathResolver = new click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver();
|
||||||
|
List<String> dynamicClasspath = classpathResolver.resolveClasspath(projectRoot);
|
||||||
|
if (!dynamicClasspath.isEmpty()) {
|
||||||
|
context.setClasspath(dynamicClasspath);
|
||||||
|
log.info("Injected {} external JARs into JDT ASTParser", dynamicClasspath.size());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("Dynamic classpath resolution disabled. Use --resolve-classpath to enable.");
|
||||||
|
}
|
||||||
|
|
||||||
context.setResolveBindings(true);
|
context.setResolveBindings(true);
|
||||||
|
|
||||||
Path hintsFile = inputDir.resolve("hints.json");
|
Path hintsFile = inputDir.resolve("hints.json");
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ public class RegressionTest {
|
|||||||
root.resolve("state_machines/maven_multi_module/core-module"),
|
root.resolve("state_machines/maven_multi_module/core-module"),
|
||||||
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
|
||||||
"MavenOrderStateMachine"
|
"MavenOrderStateMachine"
|
||||||
|
),
|
||||||
|
new TestScenario(
|
||||||
|
"Complex Multi-Module Sample",
|
||||||
|
root.resolve("state_machines/complex_multi_module_sm"),
|
||||||
|
Path.of("src/test/resources/golden/StateMachineConfig"),
|
||||||
|
"StateMachineConfig"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CallGraphBuilder;
|
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.GenericEventDetector;
|
import click.kamil.springstatemachineexporter.analysis.service.GenericEventDetector;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.SpringMvcDetector;
|
import click.kamil.springstatemachineexporter.analysis.service.SpringMvcDetector;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
@@ -76,7 +76,7 @@ public class InheritedEventDetectionTest {
|
|||||||
assertThat(tp.getEvent()).isEqualTo("event");
|
assertThat(tp.getEvent()).isEqualTo("event");
|
||||||
|
|
||||||
// Build Call Chains
|
// Build Call Chains
|
||||||
CallGraphBuilder callGraphBuilder = new CallGraphBuilder(context);
|
HeuristicCallGraphEngine callGraphBuilder = new HeuristicCallGraphEngine(context);
|
||||||
List<CallChain> chains = callGraphBuilder.findChains(entryPoints, triggers);
|
List<CallChain> chains = callGraphBuilder.findChains(entryPoints, triggers);
|
||||||
|
|
||||||
assertThat(chains).describedAs("Should link ChildController.trigger to BaseController.send").hasSize(1);
|
assertThat(chains).describedAs("Should link ChildController.trigger to BaseController.send").hasSize(1);
|
||||||
|
|||||||
@@ -118,4 +118,294 @@ 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 shouldMatchBaseEventDespiteScrapedPolyEvents() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||||
|
// State machine uses the base event generically
|
||||||
|
t1.setEvent(Event.of("BaseEvent", "BaseEvent"));
|
||||||
|
|
||||||
|
// Trigger point uses "BaseEvent", but polyEvents scraped some implementation/constants
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.event("BaseEvent")
|
||||||
|
.polymorphicEvents(List.of("EVENT_A", "EVENT_B"))
|
||||||
|
.build())
|
||||||
|
.contextMachineId("testMachine")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("testMachine")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
// It should match because smEvent matches triggerEvent directly, overriding the failed poly search
|
||||||
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
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,103 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@Tag("heuristic_bean_resolution")
|
||||||
|
public class HeuristicBeanResolutionEngineTest {
|
||||||
|
|
||||||
|
private final HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeepPackageDivergenceMismatch() {
|
||||||
|
// This is the bug case: sharing a deep root, but diverging into different domains
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.corp.division.project.orders.OrderService.pay()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||||
|
|
||||||
|
// They share com.acme.corp.division.project, but diverge into orders vs payments
|
||||||
|
// This should be a strong mismatch, so it should return false
|
||||||
|
assertFalse(result, "Deep package divergence into different domains should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSameDomainMatch() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.ecommerce.orders.OrderService.process()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||||
|
|
||||||
|
// Same exact domain, should match
|
||||||
|
assertTrue(result, "Same domain should be accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubPackageMatch() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.ecommerce.orders.impl.OrderServiceImpl.process()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||||
|
|
||||||
|
// Subpackage should match
|
||||||
|
assertTrue(result, "Sub-packages of the same domain should be accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSharedDomainTermMatch() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.orders.web.OrderController.submit()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.orders.service.OrderStateMachine";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||||
|
|
||||||
|
// Different subpackages, but they share the "order" domain term and common prefix
|
||||||
|
assertTrue(result, "Divergent packages that share a domain term should be accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExplicitTargetVariableMatch() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.common.GlobalTrigger.fire()"))
|
||||||
|
.contextMachineId("paymentStateMachine") // Explicitly targets payment
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.corp.payments.PaymentStateMachine";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||||
|
|
||||||
|
// Mismatched domains, but explicit variable targeting works
|
||||||
|
assertTrue(result, "Explicit variable target matching the machine name should be accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExplicitTargetVariableMismatch() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.common.GlobalTrigger.fire()"))
|
||||||
|
.contextMachineId("paymentStateMachine") // Explicitly targets payment
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||||
|
|
||||||
|
// Explicit variable targeting completely conflicts
|
||||||
|
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Tag("dynamic_classpath_resolver")
|
||||||
|
class DynamicClasspathResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveMavenClasspathDynamically(@TempDir Path tempDir) throws IOException {
|
||||||
|
// Create dummy pom.xml
|
||||||
|
Files.createFile(tempDir.resolve("pom.xml"));
|
||||||
|
|
||||||
|
// Mock the resolver so it doesn't actually run maven, but writes the expected output file
|
||||||
|
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
|
||||||
|
@Override
|
||||||
|
protected void runProcess(ProcessBuilder pb) throws IOException {
|
||||||
|
// Pretend maven executed and generated the cp.txt
|
||||||
|
String cp = "/fake/m2/repo/spring-core.jar" + File.pathSeparator + "/fake/m2/repo/spring-context.jar";
|
||||||
|
|
||||||
|
String outputFileArg = pb.command().stream()
|
||||||
|
.filter(arg -> arg.startsWith("-Dmdep.outputFile="))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
Path outputFile = Path.of(outputFileArg.substring("-Dmdep.outputFile=".length()));
|
||||||
|
|
||||||
|
Files.writeString(outputFile, cp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<String> classpath = resolver.resolveClasspath(tempDir);
|
||||||
|
|
||||||
|
assertThat(classpath).containsExactly(
|
||||||
|
"/fake/m2/repo/spring-core.jar",
|
||||||
|
"/fake/m2/repo/spring-context.jar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveGradleClasspathDynamically(@TempDir Path tempDir) throws IOException {
|
||||||
|
// Create dummy build.gradle
|
||||||
|
Files.createFile(tempDir.resolve("build.gradle"));
|
||||||
|
|
||||||
|
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
|
||||||
|
@Override
|
||||||
|
protected String runProcessAndGetOutput(ProcessBuilder pb) {
|
||||||
|
// Pretend gradle executed and printed the marker
|
||||||
|
String cp = "/fake/gradle/caches/spring-core.jar" + File.pathSeparator + "/fake/gradle/caches/spring-context.jar";
|
||||||
|
return "Some noisy gradle output\n" +
|
||||||
|
"SME_CLASSPATH_MARKER:" + cp + "\n" +
|
||||||
|
"BUILD SUCCESSFUL\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<String> classpath = resolver.resolveClasspath(tempDir);
|
||||||
|
|
||||||
|
assertThat(classpath).containsExactly(
|
||||||
|
"/fake/gradle/caches/spring-core.jar",
|
||||||
|
"/fake/gradle/caches/spring-context.jar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEmptyListIfNoBuildFileFound(@TempDir Path tempDir) {
|
||||||
|
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
|
||||||
|
List<String> classpath = resolver.resolveClasspath(tempDir);
|
||||||
|
assertThat(classpath).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Tag("control_flow_sibling_avoidance")
|
||||||
|
class GenericEventDetectorControlFlowTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDetectStateFromDirectIfWrapper(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderService {
|
||||||
|
private StateMachine sm;
|
||||||
|
public void processOrder(OrderState state) {
|
||||||
|
if (state == OrderState.PENDING) {
|
||||||
|
sm.sendEvent(OrderEvent.PAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvent e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderState { PENDING }
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||||
|
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
|
||||||
|
|
||||||
|
assertThat(triggers).hasSize(1);
|
||||||
|
TriggerPoint tp = triggers.get(0);
|
||||||
|
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
|
||||||
|
assertThat(tp.getSourceState()).isEqualTo("PENDING");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDetectStateFromGuardClauseSibling(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderService {
|
||||||
|
private StateMachine sm;
|
||||||
|
public void processOrder(OrderState state) {
|
||||||
|
if (state != OrderState.PENDING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.sendEvent(OrderEvent.PAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvent e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderState { PENDING }
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||||
|
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
|
||||||
|
|
||||||
|
assertThat(triggers).hasSize(1);
|
||||||
|
TriggerPoint tp = triggers.get(0);
|
||||||
|
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
|
||||||
|
assertThat(tp.getSourceState()).isEqualTo("PENDING");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDetectStateFromSwitchCaseSibling(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderService {
|
||||||
|
private StateMachine sm;
|
||||||
|
public void processOrder(OrderState state) {
|
||||||
|
switch (state) {
|
||||||
|
case PENDING:
|
||||||
|
sm.sendEvent(OrderEvent.PAY);
|
||||||
|
break;
|
||||||
|
case PAID:
|
||||||
|
sm.sendEvent(OrderEvent.SHIP);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvent e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderState { PENDING, PAID }
|
||||||
|
enum OrderEvent { PAY, SHIP }
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||||
|
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
|
||||||
|
|
||||||
|
assertThat(triggers).hasSize(2);
|
||||||
|
|
||||||
|
TriggerPoint payTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.PAY")).findFirst().get();
|
||||||
|
assertThat(payTrigger.getSourceState()).isEqualTo("PENDING");
|
||||||
|
|
||||||
|
TriggerPoint shipTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.SHIP")).findFirst().get();
|
||||||
|
assertThat(shipTrigger.getSourceState()).isEqualTo("PAID");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,4 +44,71 @@ public class GenericEventDetectorTest {
|
|||||||
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
|
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
|
||||||
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
|
||||||
|
void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("MyEnum.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public enum MyEnum {\n" +
|
||||||
|
" STATE_A, STATE_B;\n" +
|
||||||
|
"}\n");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("MyService.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"import org.springframework.statemachine.StateMachine;\n" +
|
||||||
|
"public class MyService {\n" +
|
||||||
|
" private StateMachine<String, String> stateMachine;\n" +
|
||||||
|
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
|
||||||
|
" public void trigger() {\n" +
|
||||||
|
" stateMachine.sendEvent(this.getEvent());\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}\n");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
|
||||||
|
|
||||||
|
CompilationUnit serviceCu = context.getCompilationUnits().stream()
|
||||||
|
.filter(cu -> cu.getJavaElement() == null || cu.getJavaElement().getElementName().contains("MyService"))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
|
||||||
|
List<TriggerPoint> triggers = detector.detect(serviceCu);
|
||||||
|
|
||||||
|
System.out.println("TRIGGERS: " + triggers);
|
||||||
|
assertThat(triggers).hasSize(2);
|
||||||
|
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_A"));
|
||||||
|
assertThat(triggers).anyMatch(t -> t.getEvent().equals("com.example.MyEnum.STATE_B"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package click.kamil.springstatemachineexporter.exporter;
|
package click.kamil.springstatemachineexporter.exporter;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import click.kamil.springstatemachineexporter.model.State;
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Guard;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Action;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -15,53 +15,41 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
class PlantUmlTest {
|
class PlantUmlTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFormatTransitionsWithDifferentEnumFormats() {
|
void testExportSanitizesLabels() {
|
||||||
// Given
|
PlantUml plantUml = new PlantUml();
|
||||||
PlantUml exporter = new PlantUml();
|
ExportOptions options = ExportOptions.builder()
|
||||||
Transition transition = new Transition();
|
.embedIdentifiers(true)
|
||||||
transition.setType(TransitionType.EXTERNAL);
|
.useLambdaGuards(false)
|
||||||
transition.setSourceStates(List.of(State.of("States.S1", "com.example.States.S1")));
|
|
||||||
transition.setTargetStates(List.of(State.of("States.S2", "com.example.States.S2")));
|
|
||||||
transition.setEvent(Event.of("Events.E1", "com.example.Events.E1"));
|
|
||||||
|
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
|
||||||
.name("TestSM")
|
|
||||||
.transitions(List.of(transition))
|
|
||||||
.startStates(Set.of("States.S1"))
|
|
||||||
.endStates(Set.of("States.S2"))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Test FN (Full Name - Default)
|
State state1 = State.of("State1");
|
||||||
ExportOptions optionsFn = ExportOptions.builder()
|
State state2 = State.of("State2");
|
||||||
.stateFormat(EnumFormat.fn)
|
|
||||||
.eventFormat(EnumFormat.fn)
|
|
||||||
.build();
|
|
||||||
String pumlFn = exporter.export(result, optionsFn);
|
|
||||||
assertThat(pumlFn)
|
|
||||||
.contains("States.S1 -[#")
|
|
||||||
.contains("]-> States.S2")
|
|
||||||
.contains(" : Events.E1");
|
|
||||||
|
|
||||||
// Test FQN (Fully Qualified Name)
|
Transition t = new Transition();
|
||||||
ExportOptions optionsFqn = ExportOptions.builder()
|
t.setType(TransitionType.EXTERNAL);
|
||||||
.stateFormat(EnumFormat.fqn)
|
t.setSourceStates(List.of(state1));
|
||||||
.eventFormat(EnumFormat.fqn)
|
t.setTargetStates(List.of(state2));
|
||||||
.build();
|
|
||||||
String pumlFqn = exporter.export(result, optionsFqn);
|
|
||||||
assertThat(pumlFqn)
|
|
||||||
.contains("com.example.States.S1 -[#")
|
|
||||||
.contains("]-> com.example.States.S2")
|
|
||||||
.contains(" : com.example.Events.E1");
|
|
||||||
|
|
||||||
// Test SN (Short Name)
|
// Add a guard and action containing problematic characters
|
||||||
ExportOptions optionsSn = ExportOptions.builder()
|
t.setGuard(Guard.of("list.size() < 5", false, null, null, null, null));
|
||||||
.stateFormat(EnumFormat.sn)
|
t.setActions(List.of(Action.of("doSomething(new int[]{1, 2})", false, null, null, null, null)));
|
||||||
.eventFormat(EnumFormat.sn)
|
|
||||||
.build();
|
String result = plantUml.export(
|
||||||
String pumlSn = exporter.export(result, optionsSn);
|
"TestMachine",
|
||||||
assertThat(pumlSn)
|
List.of(t),
|
||||||
.contains("S1 -[#")
|
Set.of("State1"),
|
||||||
.contains("]-> S2")
|
Set.of("State2"),
|
||||||
.contains(" : E1");
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that < and > are replaced with HTML entities
|
||||||
|
assertThat(result).doesNotContain("< 5");
|
||||||
|
assertThat(result).contains("< 5");
|
||||||
|
|
||||||
|
// Verify that [ and ] are replaced with HTML entities inside the link
|
||||||
|
assertThat(result).doesNotContain("[list.size() < 5]");
|
||||||
|
assertThat(result).doesNotContain("new int[]{1, 2}");
|
||||||
|
assertThat(result).contains("[list.size() < 5]");
|
||||||
|
assertThat(result).contains("new int[]{1, 2}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
@@ -34,21 +34,21 @@ state States.STATE14 <<choice>>
|
|||||||
state States.STATE9 <<choice>>
|
state States.STATE9 <<choice>>
|
||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> <<e_Events_EVENT10>> : Events.EVENT10
|
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> : Events.EVENT10
|
||||||
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> <<e_Events_EVENT11>> : Events.EVENT11
|
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> : Events.EVENT11
|
||||||
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> <<e_Events_EVENT12>> : Events.EVENT12
|
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> : Events.EVENT12
|
||||||
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> <<e_Events_EVENT13>> : Events.EVENT13
|
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> : Events.EVENT13
|
||||||
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> <<e_Events_EVENT14>> : Events.EVENT14
|
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> : Events.EVENT14
|
||||||
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> <<e_Events_EVENT15>> : Events.EVENT15
|
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> : Events.EVENT15
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE18 <<choice_type>> : [guardVarEquals("value2")] (order=1)
|
States.STATE16 -[#FF6347,bold]-> States.STATE18 <<choice_type>> : [guardVarEquals("value2")] (order=1)
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE19 <<choice_type>> : (order=2)
|
States.STATE16 -[#FF6347,bold]-> States.STATE19 <<choice_type>> : (order=2)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "PLACE_ORDER",
|
"event" : "PLACE_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||||
@@ -17,7 +18,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "CANCEL_ORDER",
|
"event" : "CANCEL_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||||
@@ -26,7 +28,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 21
|
"lineNumber" : 21,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "PAY_ORDER",
|
"event" : "PAY_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||||
@@ -35,7 +38,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "SHIP_ORDER",
|
"event" : "SHIP_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||||
@@ -44,7 +48,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "RETURN_ORDER",
|
"event" : "RETURN_ORDER",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||||
@@ -53,7 +58,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -137,7 +143,7 @@
|
|||||||
"name" : "JMS: shipping.queue",
|
"name" : "JMS: shipping.queue",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||||
"methodName" : "onShippingReady",
|
"methodName" : "onShippingReady",
|
||||||
"sourceFile" : null,
|
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"protocol" : "JMS",
|
"protocol" : "JMS",
|
||||||
"destination" : "shipping.queue"
|
"destination" : "shipping.queue"
|
||||||
@@ -152,7 +158,7 @@
|
|||||||
"name" : "RABBIT: returns.queue",
|
"name" : "RABBIT: returns.queue",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||||
"methodName" : "onReturn",
|
"methodName" : "onReturn",
|
||||||
"sourceFile" : null,
|
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"protocol" : "RABBIT",
|
"protocol" : "RABBIT",
|
||||||
"destination" : "returns.queue"
|
"destination" : "returns.queue"
|
||||||
@@ -185,7 +191,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -219,7 +226,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 21
|
"lineNumber" : 21,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -248,10 +256,11 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ ]
|
"matchedTransitions" : null
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -278,7 +287,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -292,7 +302,7 @@
|
|||||||
"name" : "JMS: shipping.queue",
|
"name" : "JMS: shipping.queue",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||||
"methodName" : "onShippingReady",
|
"methodName" : "onShippingReady",
|
||||||
"sourceFile" : null,
|
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"protocol" : "JMS",
|
"protocol" : "JMS",
|
||||||
"destination" : "shipping.queue"
|
"destination" : "shipping.queue"
|
||||||
@@ -312,7 +322,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -326,7 +337,7 @@
|
|||||||
"name" : "RABBIT: returns.queue",
|
"name" : "RABBIT: returns.queue",
|
||||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||||
"methodName" : "onReturn",
|
"methodName" : "onReturn",
|
||||||
"sourceFile" : null,
|
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"protocol" : "RABBIT",
|
"protocol" : "RABBIT",
|
||||||
"destination" : "returns.queue"
|
"destination" : "returns.queue"
|
||||||
@@ -346,7 +357,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17
|
"lineNumber" : 17,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -25,14 +25,14 @@ skinparam svgLinkTarget _self
|
|||||||
|
|
||||||
state CHECK_AVAILABILITY <<choice>>
|
state CHECK_AVAILABILITY <<choice>>
|
||||||
|
|
||||||
NEW -[#1E90FF,bold]-> CHECK_AVAILABILITY <<external>> <<e_PLACE_ORDER>> : PLACE_ORDER
|
NEW -[#1E90FF,bold]-> CHECK_AVAILABILITY <<external>> : PLACE_ORDER
|
||||||
CHECK_AVAILABILITY -[#FF6347,bold]-> PENDING_PAYMENT <<choice_type>> : [λ] (order=0)
|
CHECK_AVAILABILITY -[#FF6347,bold]-> PENDING_PAYMENT <<choice_type>> : [λ] (order=0)
|
||||||
CHECK_AVAILABILITY -[#FF6347,bold]-> CANCELLED <<choice_type>> : (order=1)
|
CHECK_AVAILABILITY -[#FF6347,bold]-> CANCELLED <<choice_type>> : (order=1)
|
||||||
PENDING_PAYMENT -[#1E90FF,bold]-> PAID <<external>> <<e_PAY_ORDER>> : PAY_ORDER
|
PENDING_PAYMENT -[#1E90FF,bold]-> PAID <<external>> : PAY_ORDER
|
||||||
PAID -[#1E90FF,bold]-> SHIPPED <<external>> <<e_SHIP_ORDER>> : SHIP_ORDER
|
PAID -[#1E90FF,bold]-> SHIPPED <<external>> : SHIP_ORDER
|
||||||
SHIPPED -[#1E90FF,bold]-> DELIVERED <<external>> <<e_FINALIZE>> : FINALIZE
|
SHIPPED -[#1E90FF,bold]-> DELIVERED <<external>> : FINALIZE
|
||||||
PAID -[#1E90FF,bold]-> CANCELLED <<external>> <<e_CANCEL_ORDER>> : CANCEL_ORDER
|
PAID -[#1E90FF,bold]-> CANCELLED <<external>> : CANCEL_ORDER
|
||||||
DELIVERED -[#1E90FF,bold]-> RETURNED <<external>> <<e_RETURN_ORDER>> : RETURN_ORDER
|
DELIVERED -[#1E90FF,bold]-> RETURNED <<external>> : RETURN_ORDER
|
||||||
|
|
||||||
CANCELLED --> [*]
|
CANCELLED --> [*]
|
||||||
DELIVERED --> [*]
|
DELIVERED --> [*]
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "AUDIT_EVENT",
|
"event" : "AUDIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
||||||
@@ -17,7 +18,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
"event" : "EXTERNAL_TRIGGER",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -26,7 +28,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "SUBMIT_EVENT",
|
"event" : "SUBMIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -35,7 +38,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "CANCEL_EVENT",
|
"event" : "CANCEL_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -44,7 +48,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "[LIFECYCLE:RESTORE]",
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -53,7 +58,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "REACTIVE_EVENT",
|
"event" : "REACTIVE_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||||
@@ -62,7 +68,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -160,7 +167,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -190,7 +198,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -220,7 +229,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -250,7 +260,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -284,10 +295,11 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : "orderId",
|
"contextMachineId" : "orderId",
|
||||||
"matchedTransitions" : [ ]
|
"matchedTransitions" : null
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -314,7 +326,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -343,7 +356,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
@@ -24,12 +24,12 @@ skinparam svgLinkTarget _self
|
|||||||
[*] --> INIT_STATE
|
[*] --> INIT_STATE
|
||||||
|
|
||||||
|
|
||||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_SUBMIT_EVENT>> : SUBMIT_EVENT
|
START -[#1E90FF,bold]-> PROCESSING <<external>> : SUBMIT_EVENT
|
||||||
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> <<e_FINISH>> : FINISH
|
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> : FINISH
|
||||||
PROCESSING -[#1E90FF,bold]-> CANCELLED <<external>> <<e_CANCEL_EVENT>> : CANCEL_EVENT
|
PROCESSING -[#1E90FF,bold]-> CANCELLED <<external>> : CANCEL_EVENT
|
||||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_REACTIVE_EVENT>> : REACTIVE_EVENT
|
START -[#1E90FF,bold]-> PROCESSING <<external>> : REACTIVE_EVENT
|
||||||
START -[#1E90FF,bold]-> START <<external>> <<e_AUDIT_EVENT>> : AUDIT_EVENT
|
START -[#1E90FF,bold]-> START <<external>> : AUDIT_EVENT
|
||||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_EXTERNAL_TRIGGER>> : EXTERNAL_TRIGGER
|
START -[#1E90FF,bold]-> PROCESSING <<external>> : EXTERNAL_TRIGGER
|
||||||
|
|
||||||
CANCELLED --> [*]
|
CANCELLED --> [*]
|
||||||
COMPLETED --> [*]
|
COMPLETED --> [*]
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "AUDIT_EVENT",
|
"event" : "AUDIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
"className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
|
||||||
@@ -17,7 +18,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
"event" : "EXTERNAL_TRIGGER",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -26,7 +28,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "SUBMIT_EVENT",
|
"event" : "SUBMIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -35,7 +38,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "CANCEL_EVENT",
|
"event" : "CANCEL_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -44,7 +48,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "[LIFECYCLE:RESTORE]",
|
"event" : "[LIFECYCLE:RESTORE]",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -53,7 +58,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "REACTIVE_EVENT",
|
"event" : "REACTIVE_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||||
@@ -62,7 +68,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -160,7 +167,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19
|
"lineNumber" : 19,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -190,7 +198,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20
|
"lineNumber" : 20,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -220,7 +229,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -250,7 +260,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 34
|
"lineNumber" : 34,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -284,10 +295,11 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 29
|
"lineNumber" : 29,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : "orderId",
|
"contextMachineId" : "orderId",
|
||||||
"matchedTransitions" : [ ]
|
"matchedTransitions" : null
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -314,7 +326,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -343,7 +356,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16
|
"lineNumber" : 16,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -24,12 +24,12 @@ skinparam svgLinkTarget _self
|
|||||||
[*] --> PROD_INITIAL
|
[*] --> PROD_INITIAL
|
||||||
|
|
||||||
|
|
||||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_SUBMIT_EVENT>> : SUBMIT_EVENT
|
START -[#1E90FF,bold]-> PROCESSING <<external>> : SUBMIT_EVENT
|
||||||
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> <<e_FINISH>> : FINISH
|
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> : FINISH
|
||||||
PROCESSING -[#1E90FF,bold]-> CANCELLED <<external>> <<e_CANCEL_EVENT>> : CANCEL_EVENT
|
PROCESSING -[#1E90FF,bold]-> CANCELLED <<external>> : CANCEL_EVENT
|
||||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_REACTIVE_EVENT>> : REACTIVE_EVENT
|
START -[#1E90FF,bold]-> PROCESSING <<external>> : REACTIVE_EVENT
|
||||||
START -[#1E90FF,bold]-> START <<external>> <<e_AUDIT_EVENT>> : AUDIT_EVENT
|
START -[#1E90FF,bold]-> START <<external>> : AUDIT_EVENT
|
||||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_EXTERNAL_TRIGGER>> : EXTERNAL_TRIGGER
|
START -[#1E90FF,bold]-> PROCESSING <<external>> : EXTERNAL_TRIGGER
|
||||||
|
|
||||||
CANCELLED --> [*]
|
CANCELLED --> [*]
|
||||||
COMPLETED --> [*]
|
COMPLETED --> [*]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
@@ -36,27 +36,27 @@ state States.STATE9 <<choice>>
|
|||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
state States.STATE2 <<choice>>
|
state States.STATE2 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> <<e_Events_EVENT10>> : Events.EVENT10
|
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> : Events.EVENT10
|
||||||
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> <<e_Events_EVENT11>> : Events.EVENT11
|
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> : Events.EVENT11
|
||||||
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> <<e_Events_EVENT12>> : Events.EVENT12
|
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> : Events.EVENT12
|
||||||
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> <<e_Events_EVENT13>> : Events.EVENT13
|
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> : Events.EVENT13
|
||||||
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> <<e_Events_EVENT14>> : Events.EVENT14
|
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> : Events.EVENT14
|
||||||
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> <<e_Events_EVENT15>> : Events.EVENT15
|
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> : Events.EVENT15
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> : Events.EVENTY
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
@@ -82,12 +82,12 @@ States.STATE9 -[#FF6347,bold]-> States.STATE7 <<choice_type>> : [guardVarEquals(
|
|||||||
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> <<e_Events_EVENTX>> : Events.EVENTX
|
States.STATE2 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> : Events.EVENTX
|
||||||
States.STATE2 -[#FF6347,bold]-> States.STATE1 <<choice_type>> : [guardEventHeaderEquals("header1","foo")] (order=0)
|
States.STATE2 -[#FF6347,bold]-> States.STATE1 <<choice_type>> : [guardEventHeaderEquals("header1","foo")] (order=0)
|
||||||
States.STATE2 -[#FF6347,bold]-> States.STATE_EXTRA_1 <<choice_type>> : (order=1)
|
States.STATE2 -[#FF6347,bold]-> States.STATE_EXTRA_1 <<choice_type>> : (order=1)
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.STATE_EXTRA_3 <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.STATE_EXTRA_3 <<external>> : Events.EVENTY
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
|
|
||||||
States.STATEZ --> [*]
|
States.STATEZ --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 257 KiB |
@@ -36,27 +36,27 @@ state States.STATE9 <<choice>>
|
|||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
state States.STATE_EXTRA_1_2 <<choice>>
|
state States.STATE_EXTRA_1_2 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> <<e_Events_EVENT10>> : Events.EVENT10
|
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> : Events.EVENT10
|
||||||
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> <<e_Events_EVENT11>> : Events.EVENT11
|
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> : Events.EVENT11
|
||||||
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> <<e_Events_EVENT12>> : Events.EVENT12
|
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> : Events.EVENT12
|
||||||
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> <<e_Events_EVENT13>> : Events.EVENT13
|
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> : Events.EVENT13
|
||||||
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> <<e_Events_EVENT14>> : Events.EVENT14
|
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> : Events.EVENT14
|
||||||
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> <<e_Events_EVENT15>> : Events.EVENT15
|
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> : Events.EVENT15
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> : Events.EVENTY
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
@@ -82,13 +82,13 @@ States.STATE9 -[#FF6347,bold]-> States.STATE7 <<choice_type>> : [guardVarEquals(
|
|||||||
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE_EXTRA_1_1 <<external>> <<e_Events_EVENT_1_1>> : Events.EVENT_1_1
|
States.STATE9 -[#1E90FF,bold]-> States.STATE_EXTRA_1_1 <<external>> : Events.EVENT_1_1
|
||||||
States.STATE_EXTRA_1_2 -[#FF6347,bold]-> States.STATE_EXTRA_1_1 <<choice_type>> : [guardEventHeaderEquals("header1","foo")] (order=0)
|
States.STATE_EXTRA_1_2 -[#FF6347,bold]-> States.STATE_EXTRA_1_1 <<choice_type>> : [guardEventHeaderEquals("header1","foo")] (order=0)
|
||||||
States.STATE_EXTRA_1_2 -[#FF6347,bold]-> States.STATE_EXTRA_1_3 <<choice_type>> : (order=1)
|
States.STATE_EXTRA_1_2 -[#FF6347,bold]-> States.STATE_EXTRA_1_3 <<choice_type>> : (order=1)
|
||||||
States.STATE_EXTRA_1_3 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> <<e_Events_EVENT_1_2>> : Events.EVENT_1_2
|
States.STATE_EXTRA_1_3 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> : Events.EVENT_1_2
|
||||||
States.STATE_EXTRA_1_1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_1_1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
States.STATE_EXTRA_1_2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_1_2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
|
|
||||||
States.STATEZ --> [*]
|
States.STATEZ --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@@ -24,14 +24,14 @@ skinparam svgLinkTarget _self
|
|||||||
[*] --> States.START
|
[*] --> States.START
|
||||||
|
|
||||||
|
|
||||||
States.START -[#1E90FF,bold]-> States.FORK <<external>> <<e_Events_TO_FORK>> : Events.TO_FORK
|
States.START -[#1E90FF,bold]-> States.FORK <<external>> : Events.TO_FORK
|
||||||
States.FORK -[#20B2AA,bold]-> States.REGION1_STATE1 <<fork>>
|
States.FORK -[#20B2AA,bold]-> States.REGION1_STATE1 <<fork>>
|
||||||
States.FORK -[#20B2AA,bold]-> States.REGION2_STATE1 <<fork>>
|
States.FORK -[#20B2AA,bold]-> States.REGION2_STATE1 <<fork>>
|
||||||
States.REGION1_STATE1 -[#1E90FF,bold]-> States.REGION1_STATE2 <<external>> <<e_Events_R1_NEXT>> : Events.R1_NEXT
|
States.REGION1_STATE1 -[#1E90FF,bold]-> States.REGION1_STATE2 <<external>> : Events.R1_NEXT
|
||||||
States.REGION2_STATE1 -[#1E90FF,bold]-> States.REGION2_STATE2 <<external>> <<e_Events_R2_NEXT>> : Events.R2_NEXT
|
States.REGION2_STATE1 -[#1E90FF,bold]-> States.REGION2_STATE2 <<external>> : Events.R2_NEXT
|
||||||
States.REGION1_STATE2 -[#8A2BE2,bold]-> States.JOIN <<join>>
|
States.REGION1_STATE2 -[#8A2BE2,bold]-> States.JOIN <<join>>
|
||||||
States.REGION2_STATE2 -[#8A2BE2,bold]-> States.JOIN <<join>>
|
States.REGION2_STATE2 -[#8A2BE2,bold]-> States.JOIN <<join>>
|
||||||
States.JOIN -[#1E90FF,bold]-> States.END <<external>> <<e_Events_TO_END>> : Events.TO_END
|
States.JOIN -[#1E90FF,bold]-> States.END <<external>> : Events.TO_END
|
||||||
|
|
||||||
States.END --> [*]
|
States.END --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -27,21 +27,21 @@ state States.STATEY <<choice>>
|
|||||||
state States.STATE9 <<choice>>
|
state States.STATE9 <<choice>>
|
||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> : Events.EVENTY
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
||||||
States.STATE9 -[#FF6347,bold]-> States.STATE8 <<choice_type>> : [guardVarEquals("stepBack")] (order=0)
|
States.STATE9 -[#FF6347,bold]-> States.STATE8 <<choice_type>> : [guardVarEquals("stepBack")] (order=0)
|
||||||
@@ -49,9 +49,9 @@ States.STATE9 -[#FF6347,bold]-> States.STATE7 <<choice_type>> : [guardVarEquals(
|
|||||||
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE_EXTRA_2 <<external>> <<e_Events_EVENT_1_2>> : Events.EVENT_1_2
|
States.STATE3 -[#1E90FF,bold]-> States.STATE_EXTRA_2 <<external>> : Events.EVENT_1_2
|
||||||
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.STATE_EXTRA_3 <<external>> <<e_Events_EVENT_1_2>> : Events.EVENT_1_2
|
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.STATE_EXTRA_3 <<external>> : Events.EVENT_1_2
|
||||||
States.STATE_EXTRA_3 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE_EXTRA_3 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
|
|
||||||
States.STATEZ --> [*]
|
States.STATEZ --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
@@ -27,21 +27,21 @@ state States.STATEY <<choice>>
|
|||||||
state States.STATE9 <<choice>>
|
state States.STATE9 <<choice>>
|
||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> : Events.EVENTY
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
||||||
States.STATE9 -[#FF6347,bold]-> States.STATE8 <<choice_type>> : [guardVarEquals("stepBack")] (order=0)
|
States.STATE9 -[#FF6347,bold]-> States.STATE8 <<choice_type>> : [guardVarEquals("stepBack")] (order=0)
|
||||||
@@ -49,10 +49,10 @@ States.STATE9 -[#FF6347,bold]-> States.STATE7 <<choice_type>> : [guardVarEquals(
|
|||||||
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> <<e_Events_EVENT_1_2>> : Events.EVENT_1_2
|
States.STATE3 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> : Events.EVENT_1_2
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.STATE_EXTRA_2 <<external>> <<e_Events_EVENT_1_3>> : Events.EVENT_1_3
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.STATE_EXTRA_2 <<external>> : Events.EVENT_1_3
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
|
|
||||||
States.STATEZ --> [*]
|
States.STATEZ --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 15
|
"lineNumber" : 15,
|
||||||
|
"polymorphicEvents" : null
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -55,7 +56,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 15
|
"lineNumber" : 15,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -24,7 +24,7 @@ skinparam svgLinkTarget _self
|
|||||||
[*] --> START
|
[*] --> START
|
||||||
|
|
||||||
|
|
||||||
START -[#1E90FF,bold]-> WORKING <<external>> <<e_INHERITED_SUBMIT>> : INHERITED_SUBMIT
|
START -[#1E90FF,bold]-> WORKING <<external>> : INHERITED_SUBMIT
|
||||||
|
|
||||||
WORKING --> [*]
|
WORKING --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -25,19 +25,19 @@ skinparam svgLinkTarget _self
|
|||||||
|
|
||||||
state OrderStates.PAID1 <<choice>>
|
state OrderStates.PAID1 <<choice>>
|
||||||
|
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID <<external>> <<e_OrderEvents_PAY>> : OrderEvents.PAY
|
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID <<external>> : OrderEvents.PAY
|
||||||
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.FULFILLED <<external>> <<e_OrderEvents_FULFILL>> : OrderEvents.FULFILL
|
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.FULFILLED <<external>> : OrderEvents.FULFILL
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> <<e_OrderEvents_CANCEL>> : OrderEvents.CANCEL [λ]
|
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.CANCEL [λ]
|
||||||
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> <<e_OrderEvents_CANCEL>> : OrderEvents.CANCEL
|
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.CANCEL
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.SUBMITTED <<external>> <<e_OrderEvents_ABCD>> : OrderEvents.ABCD
|
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.SUBMITTED <<external>> : OrderEvents.ABCD
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID1 <<external>> <<e_OrderEvents_ABCD>> : OrderEvents.ABCD
|
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID1 <<external>> : OrderEvents.ABCD
|
||||||
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.PAID2 <<choice_type>> : [λ] (order=0)
|
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.PAID2 <<choice_type>> : [λ] (order=0)
|
||||||
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.PAID3 <<choice_type>> : [guard1] (order=1)
|
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.PAID3 <<choice_type>> : [guard1] (order=1)
|
||||||
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.HAPPEN <<choice_type>> : [λ] (order=2)
|
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.HAPPEN <<choice_type>> : [λ] (order=2)
|
||||||
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.CANCELED <<choice_type>> : (order=3)
|
OrderStates.PAID1 -[#FF6347,bold]-> OrderStates.CANCELED <<choice_type>> : (order=3)
|
||||||
OrderStates.PAID2 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
OrderStates.PAID2 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
||||||
OrderStates.PAID3 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
OrderStates.PAID3 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
||||||
OrderStates.PAID2 -[#1E90FF,bold]-> OrderStates.PAID2 <<external>> <<e_OrderEvents_ABCD>> : OrderEvents.ABCD / λ, action2
|
OrderStates.PAID2 -[#1E90FF,bold]-> OrderStates.PAID2 <<external>> : OrderEvents.ABCD / λ, action2
|
||||||
|
|
||||||
OrderStates.CANCELED --> [*]
|
OrderStates.CANCELED --> [*]
|
||||||
OrderStates.FULFILLED --> [*]
|
OrderStates.FULFILLED --> [*]
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 40
|
"lineNumber" : 40,
|
||||||
|
"polymorphicEvents" : null
|
||||||
}, {
|
}, {
|
||||||
"event" : "ORDER_EVENT",
|
"event" : "ORDER_EVENT",
|
||||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
|
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
|
||||||
@@ -17,7 +18,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 52
|
"lineNumber" : 52,
|
||||||
|
"polymorphicEvents" : null
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
"name" : "JMS: order.queue",
|
"name" : "JMS: order.queue",
|
||||||
"className" : "click.kamil.maven.api.JmsOrderListener",
|
"className" : "click.kamil.maven.api.JmsOrderListener",
|
||||||
"methodName" : "onMessage",
|
"methodName" : "onMessage",
|
||||||
"sourceFile" : null,
|
"sourceFile" : "api-module/src/main/java/click/kamil/maven/api/JmsOrderListener.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"protocol" : "JMS",
|
"protocol" : "JMS",
|
||||||
"destination" : "order.queue"
|
"destination" : "order.queue"
|
||||||
@@ -103,7 +105,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 40
|
"lineNumber" : 40,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ skinparam svgLinkTarget _self
|
|||||||
[*] --> INIT
|
[*] --> INIT
|
||||||
|
|
||||||
|
|
||||||
INIT -[#1E90FF,bold]-> BUSY <<external>> <<e_SUBMIT>> : SUBMIT / loggingAction()
|
INIT -[#1E90FF,bold]-> BUSY <<external>> : SUBMIT / loggingAction()
|
||||||
BUSY -[#1E90FF,bold]-> DONE <<external>> <<e_ORDER_EVENT>> : ORDER_EVENT
|
BUSY -[#1E90FF,bold]-> DONE <<external>> : ORDER_EVENT
|
||||||
|
|
||||||
DONE --> [*]
|
DONE --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
@@ -35,22 +35,22 @@ state States.STATE14 <<choice>>
|
|||||||
state States.STATE9 <<choice>>
|
state States.STATE9 <<choice>>
|
||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> <<e_Events_EVENT10>> : Events.EVENT10
|
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> : Events.EVENT10
|
||||||
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> <<e_Events_EVENT11>> : Events.EVENT11
|
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> : Events.EVENT11
|
||||||
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> <<e_Events_EVENT12>> : Events.EVENT12
|
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> : Events.EVENT12
|
||||||
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> <<e_Events_EVENT13>> : Events.EVENT13
|
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> : Events.EVENT13
|
||||||
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> <<e_Events_EVENT14>> : Events.EVENT14
|
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> : Events.EVENT14
|
||||||
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> <<e_Events_EVENT15>> : Events.EVENT15
|
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> : Events.EVENT15
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> : Events.EVENTY
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
@@ -76,7 +76,7 @@ States.STATE9 -[#FF6347,bold]-> States.STATE7 <<choice_type>> : [guardVarEquals(
|
|||||||
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> <<e_Events_EVENTX>> : Events.EVENTX
|
States.STATE5 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> : Events.EVENTX
|
||||||
|
|
||||||
States.STATEZ --> [*]
|
States.STATEZ --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -67,7 +68,8 @@
|
|||||||
"sourceModule" : null,
|
"sourceModule" : null,
|
||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18
|
"lineNumber" : 18,
|
||||||
|
"polymorphicEvents" : null
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ skinparam svgLinkTarget _self
|
|||||||
[*] --> NEW
|
[*] --> NEW
|
||||||
|
|
||||||
|
|
||||||
NEW -[#1E90FF,bold]-> PROCESSING <<external>> <<e_SUBMIT>> : SUBMIT / processAction()
|
NEW -[#1E90FF,bold]-> PROCESSING <<external>> : SUBMIT / processAction()
|
||||||
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> <<e_FINISH>> : FINISH
|
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> : FINISH
|
||||||
|
|
||||||
COMPLETED --> [*]
|
COMPLETED --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
@@ -26,11 +26,11 @@ skinparam svgLinkTarget _self
|
|||||||
state OrderStates.SUBMITTED <<choice>>
|
state OrderStates.SUBMITTED <<choice>>
|
||||||
state OrderStates.PAID <<choice>>
|
state OrderStates.PAID <<choice>>
|
||||||
|
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID <<external>> <<e_OrderEvents_PAY>> : OrderEvents.PAY
|
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID <<external>> : OrderEvents.PAY
|
||||||
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.FULFILLED <<external>> <<e_OrderEvents_FULFILL>> : OrderEvents.FULFILL
|
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.FULFILLED <<external>> : OrderEvents.FULFILL
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> <<e_OrderEvents_CANCEL>> : OrderEvents.CANCEL [λ]
|
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.CANCEL [λ]
|
||||||
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> <<e_OrderEvents_CANCEL>> : OrderEvents.CANCEL
|
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.CANCEL
|
||||||
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.SUBMITTED <<external>> <<e_OrderEvents_ABCD>> : OrderEvents.ABCD
|
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.SUBMITTED <<external>> : OrderEvents.ABCD
|
||||||
OrderStates.SUBMITTED -[#FF6347,bold]-> OrderStates.PAID2 <<choice_type>> : [λ] (order=0)
|
OrderStates.SUBMITTED -[#FF6347,bold]-> OrderStates.PAID2 <<choice_type>> : [λ] (order=0)
|
||||||
OrderStates.SUBMITTED -[#FF6347,bold]-> OrderStates.PAID3 <<choice_type>> : (order=1)
|
OrderStates.SUBMITTED -[#FF6347,bold]-> OrderStates.PAID3 <<choice_type>> : (order=1)
|
||||||
OrderStates.PAID -[#FF6347,bold]-> OrderStates.PAID1 <<choice_type>> : [λ] (order=0)
|
OrderStates.PAID -[#FF6347,bold]-> OrderStates.PAID1 <<choice_type>> : [λ] (order=0)
|
||||||
@@ -39,7 +39,7 @@ OrderStates.PAID -[#FF6347,bold]-> OrderStates.HAPPEN <<choice_type>> : [λ] (or
|
|||||||
OrderStates.PAID -[#FF6347,bold]-> OrderStates.PAID3 <<choice_type>> : (order=3)
|
OrderStates.PAID -[#FF6347,bold]-> OrderStates.PAID3 <<choice_type>> : (order=3)
|
||||||
OrderStates.PAID2 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
OrderStates.PAID2 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
||||||
OrderStates.PAID3 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
OrderStates.PAID3 -[#1E90FF,bold]-> OrderStates.CANCELED <<external>>
|
||||||
OrderStates.PAID1 -[#1E90FF,bold]-> OrderStates.PAID1 <<external>> <<e_OrderEvents_ABCD>> : OrderEvents.ABCD / λ, action2
|
OrderStates.PAID1 -[#1E90FF,bold]-> OrderStates.PAID1 <<external>> : OrderEvents.ABCD / λ, action2
|
||||||
|
|
||||||
OrderStates.CANCELED --> [*]
|
OrderStates.CANCELED --> [*]
|
||||||
OrderStates.FULFILLED --> [*]
|
OrderStates.FULFILLED --> [*]
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
digraph statemachine {
|
||||||
|
rankdir=LR;
|
||||||
|
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||||
|
edge [fontname="Arial", fontsize=10];
|
||||||
|
|
||||||
|
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||||
|
_start -> NEW;
|
||||||
|
COMPLETED [fillcolor=lightgray];
|
||||||
|
CANCELLED [fillcolor=lightgray];
|
||||||
|
NEW -> PROCESSING [label="OrderEvent.PROCESS", style="solid", color="black"];
|
||||||
|
PROCESSING -> COMPLETED [label="OrderEvent.COMPLETE", style="solid", color="black"];
|
||||||
|
NEW -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
|
||||||
|
PROCESSING -> CANCELLED [label="OrderEvent.CANCEL", style="solid", color="black"];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
{
|
||||||
|
"metadata" : {
|
||||||
|
"triggers" : [ {
|
||||||
|
"event" : "event",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : null
|
||||||
|
}, {
|
||||||
|
"event" : "eventProvider",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessageWithProvider",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 50,
|
||||||
|
"polymorphicEvents" : null
|
||||||
|
}, {
|
||||||
|
"event" : "customMessage",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendCustomMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 78,
|
||||||
|
"polymorphicEvents" : null
|
||||||
|
} ],
|
||||||
|
"entryPoints" : [ {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/process",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "processOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/process",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/complete",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "completeOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/complete",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/cancel",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "cancelOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/cancel",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/functional-complete",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "functionalCompleteOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/functional-complete",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
}, {
|
||||||
|
"type" : "RABBIT",
|
||||||
|
"name" : "RABBIT: order.custom.transition.queue",
|
||||||
|
"className" : "click.kamil.web.RabbitOrderListener",
|
||||||
|
"methodName" : "handleCustomTransition",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/RabbitOrderListener.java",
|
||||||
|
"metadata" : {
|
||||||
|
"protocol" : "RABBIT",
|
||||||
|
"destination" : "order.custom.transition.queue"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "payload",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"type" : "JMS",
|
||||||
|
"name" : "JMS: order.process.queue",
|
||||||
|
"className" : "click.kamil.web.JmsOrderListener",
|
||||||
|
"methodName" : "receiveProcessCommand",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java",
|
||||||
|
"metadata" : {
|
||||||
|
"protocol" : "JMS",
|
||||||
|
"destination" : "order.process.queue"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "message",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"type" : "JMS",
|
||||||
|
"name" : "JMS: order.cancel.queue",
|
||||||
|
"className" : "click.kamil.web.JmsOrderListener",
|
||||||
|
"methodName" : "receiveCancelCommand",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java",
|
||||||
|
"metadata" : {
|
||||||
|
"protocol" : "JMS",
|
||||||
|
"destination" : "order.cancel.queue"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "message",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
} ],
|
||||||
|
"callChains" : [ {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/process",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "processOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/process",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.OrderController.processOrder", "click.kamil.service.StateMachineServiceImpl.sendMessageWithOutbox", "click.kamil.service.StateMachineServiceImpl.sendMessage" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.PROCESS",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ]
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.NEW",
|
||||||
|
"targetState" : "OrderState.PROCESSING",
|
||||||
|
"event" : "OrderEvent.PROCESS"
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/complete",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "completeOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/complete",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.OrderController.completeOrder", "click.kamil.service.StateMachineServiceImpl.sendMessage" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.COMPLETE",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ]
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.PROCESSING",
|
||||||
|
"targetState" : "OrderState.COMPLETED",
|
||||||
|
"event" : "OrderEvent.COMPLETE"
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/cancel",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "cancelOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/cancel",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.OrderController.cancelOrder", "click.kamil.service.StateMachineServiceImpl.sendMessage" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.CANCEL",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ]
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.NEW",
|
||||||
|
"targetState" : "OrderState.CANCELLED",
|
||||||
|
"event" : "OrderEvent.CANCEL"
|
||||||
|
}, {
|
||||||
|
"sourceState" : "OrderState.PROCESSING",
|
||||||
|
"targetState" : "OrderState.CANCELLED",
|
||||||
|
"event" : "OrderEvent.CANCEL"
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "REST",
|
||||||
|
"name" : "POST /api/orders/functional-complete",
|
||||||
|
"className" : "click.kamil.web.OrderController",
|
||||||
|
"methodName" : "functionalCompleteOrder",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||||
|
"metadata" : {
|
||||||
|
"path" : "/api/orders/functional-complete",
|
||||||
|
"verb" : "POST"
|
||||||
|
},
|
||||||
|
"parameters" : [ ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.OrderController.functionalCompleteOrder", "click.kamil.web.OrderController.triggerTransitionFunction", "click.kamil.service.StateMachineServiceImpl.sendMessage" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.COMPLETE",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ]
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.PROCESSING",
|
||||||
|
"targetState" : "OrderState.COMPLETED",
|
||||||
|
"event" : "OrderEvent.COMPLETE"
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "RABBIT",
|
||||||
|
"name" : "RABBIT: order.custom.transition.queue",
|
||||||
|
"className" : "click.kamil.web.RabbitOrderListener",
|
||||||
|
"methodName" : "handleCustomTransition",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/RabbitOrderListener.java",
|
||||||
|
"metadata" : {
|
||||||
|
"protocol" : "RABBIT",
|
||||||
|
"destination" : "order.custom.transition.queue"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "payload",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.RabbitOrderListener.handleCustomTransition", "click.kamil.service.StateMachineServiceImpl.sendCustomMessage" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.PROCESS",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendCustomMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 78,
|
||||||
|
"polymorphicEvents" : [ ]
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.NEW",
|
||||||
|
"targetState" : "OrderState.PROCESSING",
|
||||||
|
"event" : "OrderEvent.PROCESS"
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "JMS",
|
||||||
|
"name" : "JMS: order.process.queue",
|
||||||
|
"className" : "click.kamil.web.JmsOrderListener",
|
||||||
|
"methodName" : "receiveProcessCommand",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java",
|
||||||
|
"metadata" : {
|
||||||
|
"protocol" : "JMS",
|
||||||
|
"destination" : "order.process.queue"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "message",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.JmsOrderListener.receiveProcessCommand", "click.kamil.service.StateMachineServiceImpl.sendMessageWithOutbox", "click.kamil.service.StateMachineServiceImpl.sendMessage" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.PROCESS",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessage",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 25,
|
||||||
|
"polymorphicEvents" : [ ]
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.NEW",
|
||||||
|
"targetState" : "OrderState.PROCESSING",
|
||||||
|
"event" : "OrderEvent.PROCESS"
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
"entryPoint" : {
|
||||||
|
"type" : "JMS",
|
||||||
|
"name" : "JMS: order.cancel.queue",
|
||||||
|
"className" : "click.kamil.web.JmsOrderListener",
|
||||||
|
"methodName" : "receiveCancelCommand",
|
||||||
|
"sourceFile" : "web/src/main/java/click/kamil/web/JmsOrderListener.java",
|
||||||
|
"metadata" : {
|
||||||
|
"protocol" : "JMS",
|
||||||
|
"destination" : "order.cancel.queue"
|
||||||
|
},
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "message",
|
||||||
|
"type" : "String",
|
||||||
|
"annotations" : [ ]
|
||||||
|
} ]
|
||||||
|
},
|
||||||
|
"methodChain" : [ "click.kamil.web.JmsOrderListener.receiveCancelCommand", "click.kamil.service.StateMachineServiceImpl.sendMessageWithProvider" ],
|
||||||
|
"triggerPoint" : {
|
||||||
|
"event" : "OrderEvent.CANCEL",
|
||||||
|
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||||
|
"methodName" : "sendMessageWithProvider",
|
||||||
|
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||||
|
"sourceModule" : null,
|
||||||
|
"stateMachineId" : null,
|
||||||
|
"sourceState" : null,
|
||||||
|
"lineNumber" : 50,
|
||||||
|
"polymorphicEvents" : [ ]
|
||||||
|
},
|
||||||
|
"contextMachineId" : null,
|
||||||
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderState.NEW",
|
||||||
|
"targetState" : "OrderState.CANCELLED",
|
||||||
|
"event" : "OrderEvent.CANCEL"
|
||||||
|
}, {
|
||||||
|
"sourceState" : "OrderState.PROCESSING",
|
||||||
|
"targetState" : "OrderState.CANCELLED",
|
||||||
|
"event" : "OrderEvent.CANCEL"
|
||||||
|
} ]
|
||||||
|
} ],
|
||||||
|
"properties" : {
|
||||||
|
"default" : { }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name" : "click.kamil.domain.StateMachineConfig",
|
||||||
|
"renderChoicesAsDiamonds" : true,
|
||||||
|
"startStates" : [ "OrderState.NEW" ],
|
||||||
|
"transitions" : [ {
|
||||||
|
"type" : "EXTERNAL",
|
||||||
|
"sourceStates" : [ {
|
||||||
|
"rawName" : "OrderState.NEW",
|
||||||
|
"fullIdentifier" : "OrderState.NEW"
|
||||||
|
} ],
|
||||||
|
"targetStates" : [ {
|
||||||
|
"rawName" : "OrderState.PROCESSING",
|
||||||
|
"fullIdentifier" : "OrderState.PROCESSING"
|
||||||
|
} ],
|
||||||
|
"event" : {
|
||||||
|
"rawName" : "OrderEvent.PROCESS",
|
||||||
|
"fullIdentifier" : "OrderEvent.PROCESS"
|
||||||
|
},
|
||||||
|
"guard" : null,
|
||||||
|
"actions" : [ ],
|
||||||
|
"order" : null
|
||||||
|
}, {
|
||||||
|
"type" : "EXTERNAL",
|
||||||
|
"sourceStates" : [ {
|
||||||
|
"rawName" : "OrderState.PROCESSING",
|
||||||
|
"fullIdentifier" : "OrderState.PROCESSING"
|
||||||
|
} ],
|
||||||
|
"targetStates" : [ {
|
||||||
|
"rawName" : "OrderState.COMPLETED",
|
||||||
|
"fullIdentifier" : "OrderState.COMPLETED"
|
||||||
|
} ],
|
||||||
|
"event" : {
|
||||||
|
"rawName" : "OrderEvent.COMPLETE",
|
||||||
|
"fullIdentifier" : "OrderEvent.COMPLETE"
|
||||||
|
},
|
||||||
|
"guard" : null,
|
||||||
|
"actions" : [ ],
|
||||||
|
"order" : null
|
||||||
|
}, {
|
||||||
|
"type" : "EXTERNAL",
|
||||||
|
"sourceStates" : [ {
|
||||||
|
"rawName" : "OrderState.NEW",
|
||||||
|
"fullIdentifier" : "OrderState.NEW"
|
||||||
|
} ],
|
||||||
|
"targetStates" : [ {
|
||||||
|
"rawName" : "OrderState.CANCELLED",
|
||||||
|
"fullIdentifier" : "OrderState.CANCELLED"
|
||||||
|
} ],
|
||||||
|
"event" : {
|
||||||
|
"rawName" : "OrderEvent.CANCEL",
|
||||||
|
"fullIdentifier" : "OrderEvent.CANCEL"
|
||||||
|
},
|
||||||
|
"guard" : null,
|
||||||
|
"actions" : [ ],
|
||||||
|
"order" : null
|
||||||
|
}, {
|
||||||
|
"type" : "EXTERNAL",
|
||||||
|
"sourceStates" : [ {
|
||||||
|
"rawName" : "OrderState.PROCESSING",
|
||||||
|
"fullIdentifier" : "OrderState.PROCESSING"
|
||||||
|
} ],
|
||||||
|
"targetStates" : [ {
|
||||||
|
"rawName" : "OrderState.CANCELLED",
|
||||||
|
"fullIdentifier" : "OrderState.CANCELLED"
|
||||||
|
} ],
|
||||||
|
"event" : {
|
||||||
|
"rawName" : "OrderEvent.CANCEL",
|
||||||
|
"fullIdentifier" : "OrderEvent.CANCEL"
|
||||||
|
},
|
||||||
|
"guard" : null,
|
||||||
|
"actions" : [ ],
|
||||||
|
"order" : null
|
||||||
|
} ],
|
||||||
|
"endStates" : [ "OrderState.COMPLETED", "OrderState.CANCELLED" ]
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@startuml
|
||||||
|
!pragma layout smetana
|
||||||
|
set separator none
|
||||||
|
hide empty description
|
||||||
|
hide stereotype
|
||||||
|
skinparam state {
|
||||||
|
BackgroundColor white
|
||||||
|
BorderColor #94a3b8
|
||||||
|
BorderThickness 1
|
||||||
|
FontName Inter
|
||||||
|
FontSize 9
|
||||||
|
FontStyle bold
|
||||||
|
RoundCorner 20
|
||||||
|
Padding 1
|
||||||
|
}
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam ArrowFontName JetBrains Mono
|
||||||
|
skinparam ArrowFontSize 8
|
||||||
|
skinparam ArrowColor #cbd5e1
|
||||||
|
skinparam ArrowThickness 1
|
||||||
|
skinparam dpi 110
|
||||||
|
skinparam svgLinkTarget _self
|
||||||
|
|
||||||
|
[*] --> OrderState.NEW
|
||||||
|
|
||||||
|
|
||||||
|
OrderState.NEW -[#1E90FF,bold]-> OrderState.PROCESSING <<external>> : OrderEvent.PROCESS
|
||||||
|
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.COMPLETED <<external>> : OrderEvent.COMPLETE
|
||||||
|
OrderState.NEW -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
|
||||||
|
OrderState.PROCESSING -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderEvent.CANCEL
|
||||||
|
|
||||||
|
OrderState.COMPLETED --> [*]
|
||||||
|
OrderState.CANCELLED --> [*]
|
||||||
|
@enduml
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
|
||||||
|
<state id="NEW">
|
||||||
|
<transition target="PROCESSING" event="OrderEvent.PROCESS"/>
|
||||||
|
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
|
||||||
|
</state>
|
||||||
|
<state id="PROCESSING">
|
||||||
|
<transition target="COMPLETED" event="OrderEvent.COMPLETE"/>
|
||||||
|
<transition target="CANCELLED" event="OrderEvent.CANCEL"/>
|
||||||
|
</state>
|
||||||
|
<state id="COMPLETED">
|
||||||
|
</state>
|
||||||
|
<state id="CANCELLED">
|
||||||
|
</state>
|
||||||
|
</scxml>
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
@@ -36,27 +36,27 @@ state States.STATE9 <<choice>>
|
|||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
state States.STATE2 <<choice>>
|
state States.STATE2 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> <<e_Events_EVENT10>> : Events.EVENT10
|
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> : Events.EVENT10
|
||||||
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> <<e_Events_EVENT11>> : Events.EVENT11
|
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> : Events.EVENT11
|
||||||
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> <<e_Events_EVENT12>> : Events.EVENT12
|
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> : Events.EVENT12
|
||||||
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> <<e_Events_EVENT13>> : Events.EVENT13
|
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> : Events.EVENT13
|
||||||
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> <<e_Events_EVENT14>> : Events.EVENT14
|
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> : Events.EVENT14
|
||||||
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> <<e_Events_EVENT15>> : Events.EVENT15
|
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> : Events.EVENT15
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> : Events.EVENTY
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
@@ -82,12 +82,12 @@ States.STATE9 -[#FF6347,bold]-> States.STATE7 <<choice_type>> : [guardVarEquals(
|
|||||||
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> <<e_Events_EVENTX>> : Events.EVENTX
|
States.STATE2 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> : Events.EVENTX
|
||||||
States.STATE2 -[#FF6347,bold]-> States.STATE1 <<choice_type>> : [guardEventHeaderEquals("header1","foo")] (order=0)
|
States.STATE2 -[#FF6347,bold]-> States.STATE1 <<choice_type>> : [guardEventHeaderEquals("header1","foo")] (order=0)
|
||||||
States.STATE2 -[#FF6347,bold]-> States.STATE_EXTRA_1 <<choice_type>> : (order=1)
|
States.STATE2 -[#FF6347,bold]-> States.STATE_EXTRA_1 <<choice_type>> : (order=1)
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.STATE_EXTRA_3 <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.STATE_EXTRA_3 <<external>> : Events.EVENTY
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
|
|
||||||
States.STATEZ --> [*]
|
States.STATEZ --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
@@ -35,27 +35,27 @@ state States.STATE14 <<choice>>
|
|||||||
state States.STATE9 <<choice>>
|
state States.STATE9 <<choice>>
|
||||||
state States.STATE8 <<choice>>
|
state States.STATE8 <<choice>>
|
||||||
|
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> <<e_Events_EVENT1>> : Events.EVENT1
|
States.STATE1 -[#1E90FF,bold]-> States.STATE2 <<external>> : Events.EVENT1
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> <<e_Events_EVENT2>> : Events.EVENT2
|
States.STATE2 -[#1E90FF,bold]-> States.STATE3 <<external>> : Events.EVENT2
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> <<e_Events_EVENT3>> : Events.EVENT3
|
States.STATE3 -[#1E90FF,bold]-> States.STATE4 <<external>> : Events.EVENT3
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> <<e_Events_EVENT4>> : Events.EVENT4
|
States.STATE4 -[#1E90FF,bold]-> States.STATE5 <<external>> : Events.EVENT4
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> <<e_Events_EVENT5>> : Events.EVENT5
|
States.STATE5 -[#1E90FF,bold]-> States.STATE6 <<external>> : Events.EVENT5
|
||||||
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> <<e_Events_EVENT6>> : Events.EVENT6
|
States.STATE6 -[#1E90FF,bold]-> States.STATE7 <<external>> : Events.EVENT6
|
||||||
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> <<e_Events_EVENT7>> : Events.EVENT7
|
States.STATE7 -[#1E90FF,bold]-> States.STATE8 <<external>> : Events.EVENT7
|
||||||
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> <<e_Events_EVENT8>> : Events.EVENT8
|
States.STATE8 -[#1E90FF,bold]-> States.STATE9 <<external>> : Events.EVENT8
|
||||||
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> <<e_Events_EVENT9>> : Events.EVENT9
|
States.STATE9 -[#1E90FF,bold]-> States.STATE10 <<external>> : Events.EVENT9
|
||||||
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> <<e_Events_EVENT10>> : Events.EVENT10
|
States.STATE10 -[#1E90FF,bold]-> States.STATE11 <<external>> : Events.EVENT10
|
||||||
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> <<e_Events_EVENT11>> : Events.EVENT11
|
States.STATE11 -[#1E90FF,bold]-> States.STATE12 <<external>> : Events.EVENT11
|
||||||
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> <<e_Events_EVENT12>> : Events.EVENT12
|
States.STATE12 -[#1E90FF,bold]-> States.STATE13 <<external>> : Events.EVENT12
|
||||||
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> <<e_Events_EVENT13>> : Events.EVENT13
|
States.STATE13 -[#1E90FF,bold]-> States.STATE14 <<external>> : Events.EVENT13
|
||||||
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> <<e_Events_EVENT14>> : Events.EVENT14
|
States.STATE14 -[#1E90FF,bold]-> States.STATE15 <<external>> : Events.EVENT14
|
||||||
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> <<e_Events_EVENT15>> : Events.EVENT15
|
States.STATE15 -[#1E90FF,bold]-> States.STATE16 <<external>> : Events.EVENT15
|
||||||
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE2 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE3 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE4 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE5 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> <<e_Events_EVENTY>> : Events.EVENTY
|
States.STATE4 -[#1E90FF,bold]-> States.STATEY <<external>> : Events.EVENTY
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATEY -[#FF6347,bold]-> States.STATEX <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
States.STATEY -[#FF6347,bold]-> States.STATEZ <<choice_type>> : (order=1)
|
||||||
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
States.STATE16 -[#FF6347,bold]-> States.STATE17 <<choice_type>> : [guardVarEquals("value1")] (order=0)
|
||||||
@@ -81,9 +81,9 @@ States.STATE9 -[#FF6347,bold]-> States.STATE7 <<choice_type>> : [guardVarEquals(
|
|||||||
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
States.STATE9 -[#FF6347,bold]-> States.STATE6 <<choice_type>> : (order=2)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
States.STATE8 -[#FF6347,bold]-> States.STATE9 <<choice_type>> : [guardVarEquals("forward9")] (order=0)
|
||||||
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
States.STATE8 -[#FF6347,bold]-> States.STATE10 <<choice_type>> : (order=1)
|
||||||
States.STATE5 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> <<e_Events_EVENTX>> : Events.EVENTX
|
States.STATE5 -[#1E90FF,bold]-> States.STATE_EXTRA_1 <<external>> : Events.EVENTX
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL>> : Events.EVENT_CANCEL
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL
|
||||||
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> <<e_Events_EVENT_CANCEL_2>> : Events.EVENT_CANCEL_2
|
States.STATE_EXTRA_1 -[#1E90FF,bold]-> States.CANCEL <<external>> : Events.EVENT_CANCEL_2
|
||||||
|
|
||||||
States.STATEZ --> [*]
|
States.STATEZ --> [*]
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -576,7 +593,7 @@
|
|||||||
if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false;
|
if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false;
|
||||||
return c.matchedTransitions.some(t => {
|
return c.matchedTransitions.some(t => {
|
||||||
const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event;
|
const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event;
|
||||||
return normalize(cEventStr) === name;
|
return normalize(cEventStr) === normalize(name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -631,7 +648,7 @@
|
|||||||
let html = `<div style="font-size:0.65rem; font-weight:700; color:#94a3b8; margin-top:8px; margin-bottom:4px; text-transform:uppercase;">Input Data</div>`;
|
let html = `<div style="font-size:0.65rem; font-weight:700; color:#94a3b8; margin-top:8px; margin-bottom:4px; text-transform:uppercase;">Input Data</div>`;
|
||||||
html += `<div class="payload-box">`;
|
html += `<div class="payload-box">`;
|
||||||
params.forEach(p => {
|
params.forEach(p => {
|
||||||
const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam"));
|
const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam") || a.endsWith("Payload") || a.endsWith("Header") || a.endsWith("Headers"));
|
||||||
const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : "";
|
const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : "";
|
||||||
html += `<div class="payload-param">
|
html += `<div class="payload-param">
|
||||||
<span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span>
|
<span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ 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
|
||||||
|
|
||||||
assertThat(html).contains("__CREATE");
|
assertThat(html).contains("__CREATE");
|
||||||
assertThat(html).contains("__ASSIGN");
|
assertThat(html).contains("__ASSIGN");
|
||||||
assertThat(html).contains("__CLOSE_TICKET");
|
assertThat(html).contains("__CLOSE_TICKET");
|
||||||
|
|||||||
33
state_machines/complex_multi_module_sm/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
33
state_machines/complex_multi_module_sm/domain/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
24
state_machines/complex_multi_module_sm/domain/pom.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>click.kamil.complex</groupId>
|
||||||
|
<artifactId>complex-multi-module-sm</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>domain</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.statemachine</groupId>
|
||||||
|
<artifactId>spring-statemachine-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package click.kamil.domain;
|
||||||
|
|
||||||
|
import org.springframework.messaging.support.GenericMessage;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class CustomStateMachineMessage<T, P> extends GenericMessage<T> {
|
||||||
|
|
||||||
|
private final P customPayload;
|
||||||
|
|
||||||
|
public CustomStateMachineMessage(T payload, P customPayload) {
|
||||||
|
super(payload);
|
||||||
|
this.customPayload = customPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomStateMachineMessage(T payload, Map<String, Object> headers, P customPayload) {
|
||||||
|
super(payload, headers);
|
||||||
|
this.customPayload = customPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public P getCustomPayload() {
|
||||||
|
return customPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package click.kamil.domain;
|
||||||
|
|
||||||
|
public interface IEvent {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package click.kamil.domain;
|
||||||
|
|
||||||
|
public interface IState {
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package click.kamil.domain;
|
||||||
|
|
||||||
|
public enum OrderEvent implements IEvent {
|
||||||
|
PROCESS, COMPLETE, CANCEL
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package click.kamil.domain;
|
||||||
|
|
||||||
|
public enum OrderState implements IState {
|
||||||
|
NEW, PROCESSING, COMPLETED, CANCELLED
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package click.kamil.domain;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.statemachine.config.EnableStateMachine;
|
||||||
|
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||||
|
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||||
|
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableStateMachine
|
||||||
|
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
|
||||||
|
states
|
||||||
|
.withStates()
|
||||||
|
.initial(OrderState.NEW)
|
||||||
|
.states(EnumSet.allOf(OrderState.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception {
|
||||||
|
transitions
|
||||||
|
.withExternal()
|
||||||
|
.source(OrderState.NEW).target(OrderState.PROCESSING).event(OrderEvent.PROCESS)
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source(OrderState.PROCESSING).target(OrderState.COMPLETED).event(OrderEvent.COMPLETE)
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source(OrderState.NEW).target(OrderState.CANCELLED).event(OrderEvent.CANCEL)
|
||||||
|
.and()
|
||||||
|
.withExternal()
|
||||||
|
.source(OrderState.PROCESSING).target(OrderState.CANCELLED).event(OrderEvent.CANCEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
state_machines/complex_multi_module_sm/pom.xml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>click.kamil.complex</groupId>
|
||||||
|
<artifactId>complex-multi-module-sm</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>domain</module>
|
||||||
|
<module>service</module>
|
||||||
|
<module>web</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<spring-boot.version>3.2.0</spring-boot.version>
|
||||||
|
<spring-statemachine.version>3.2.0</spring-statemachine.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.statemachine</groupId>
|
||||||
|
<artifactId>spring-statemachine-core</artifactId>
|
||||||
|
<version>${spring-statemachine.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>click.kamil.complex</groupId>
|
||||||
|
<artifactId>domain</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>click.kamil.complex</groupId>
|
||||||
|
<artifactId>service</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
</project>
|
||||||
33
state_machines/complex_multi_module_sm/service/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
32
state_machines/complex_multi_module_sm/service/pom.xml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>click.kamil.complex</groupId>
|
||||||
|
<artifactId>complex-multi-module-sm</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>service</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>click.kamil.complex</groupId>
|
||||||
|
<artifactId>domain</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.statemachine</groupId>
|
||||||
|
<artifactId>spring-statemachine-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-tx</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package click.kamil.service;
|
||||||
|
|
||||||
|
import click.kamil.domain.IEvent;
|
||||||
|
|
||||||
|
public interface OutboxAction<T extends IEvent> {
|
||||||
|
void onCommit(T event);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package click.kamil.service;
|
||||||
|
|
||||||
|
import click.kamil.domain.IEvent;
|
||||||
|
import click.kamil.domain.IState;
|
||||||
|
|
||||||
|
public interface StateMachineService {
|
||||||
|
<S extends IState, T extends IEvent> void sendMessage(T event);
|
||||||
|
|
||||||
|
<S extends IState, T extends IEvent, A extends OutboxAction<T>> void sendMessageWithOutbox(T event, A action);
|
||||||
|
|
||||||
|
// Quirky pattern: Bounded generic supplier instead of reflection
|
||||||
|
<T extends click.kamil.domain.OrderEvent> void sendMessageWithProvider(java.util.function.Supplier<T> eventProvider);
|
||||||
|
|
||||||
|
// Quirky pattern 2: Generic Varargs
|
||||||
|
<T extends IEvent> void sendEventsVarargs(T... events);
|
||||||
|
|
||||||
|
// Functional quirks: passing functions and method references
|
||||||
|
<T extends IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
|
||||||
|
java.util.function.Function<T, Void> transitionAction);
|
||||||
|
|
||||||
|
// Child class of Spring's GenericMessage with a generic payload
|
||||||
|
<T extends click.kamil.domain.OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage);
|
||||||
|
|
||||||
|
// Quirky pattern 3: Multiple generic bounds (Intersection Types)
|
||||||
|
<T extends IEvent & java.io.Serializable & Cloneable> T processQuirkyEvent(T event);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package click.kamil.service;
|
||||||
|
|
||||||
|
import click.kamil.domain.IEvent;
|
||||||
|
import click.kamil.domain.IState;
|
||||||
|
import click.kamil.domain.OrderEvent;
|
||||||
|
import click.kamil.domain.OrderState;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class StateMachineServiceImpl implements StateMachineService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StateMachine<OrderState, OrderEvent> stateMachine;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <S extends IState, T extends IEvent> void sendMessage(T event) {
|
||||||
|
if (event instanceof OrderEvent) {
|
||||||
|
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload((OrderEvent) event).build())).subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public <S extends IState, T extends IEvent, A extends OutboxAction<T>> void sendMessageWithOutbox(T event, A action) {
|
||||||
|
sendMessage(event);
|
||||||
|
|
||||||
|
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
action.onCommit(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
action.onCommit(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends OrderEvent> void sendMessageWithProvider(java.util.function.Supplier<T> eventProvider) {
|
||||||
|
T event = eventProvider.get();
|
||||||
|
if (event != null) {
|
||||||
|
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload((OrderEvent) event).build())).subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SafeVarargs
|
||||||
|
public final <T extends IEvent> void sendEventsVarargs(T... events) {
|
||||||
|
for (T event : events) {
|
||||||
|
sendMessage(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends IEvent> void executeFunctionalTransition(java.util.function.Supplier<T> eventSupplier,
|
||||||
|
java.util.function.Function<T, Void> transitionAction) {
|
||||||
|
// Resolve the event from the getter / supplier function
|
||||||
|
T event = eventSupplier.get();
|
||||||
|
// Apply the transition via the provided function / method reference
|
||||||
|
transitionAction.apply(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends OrderEvent, P> void sendCustomMessage(click.kamil.domain.CustomStateMachineMessage<T, P> customMessage) {
|
||||||
|
// Because CustomStateMachineMessage extends GenericMessage<T> and implements Message<T>,
|
||||||
|
// we can pass it directly to the state machine as long as T resolves to OrderEvent!
|
||||||
|
|
||||||
|
// We cast it to Message<OrderEvent> to satisfy Mono's strict type requirements
|
||||||
|
org.springframework.messaging.Message<OrderEvent> msg = (org.springframework.messaging.Message<OrderEvent>) customMessage;
|
||||||
|
stateMachine.sendEvent(Mono.just(msg)).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends IEvent & java.io.Serializable & Cloneable> T processQuirkyEvent(T event) {
|
||||||
|
// Quirky pattern 3: Multiple bounds, returning the same generic type
|
||||||
|
sendMessage(event);
|
||||||
|
return event; // returns something that is guaranteed to be IEvent, Serializable, AND Cloneable
|
||||||
|
}
|
||||||
|
}
|
||||||