diff --git a/plan-extended-anaylis/PLAN.md b/plan-extended-anaylis/PLAN.md
index 81be21f..bdaebbe 100644
--- a/plan-extended-anaylis/PLAN.md
+++ b/plan-extended-anaylis/PLAN.md
@@ -25,9 +25,16 @@
- [ ] **ConfigurableTriggerDetector**: Support user-defined patterns for custom frameworks.
## Phase 4: Call Graph Integration
-- [ ] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
+- [x] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
- [ ] Support inheritance in call graph resolution (using JDT bindings to resolve method overrides).
+## Phase 4.5: Hierarchical Robustness (Inheritance Support)
+- [ ] **Hierarchical Annotation Lookup**: Update `SpringMvcDetector` to scan interfaces and superclasses for inherited mapping annotations.
+- [ ] **Interface Implementation Mapping**: Enhance `CodebaseContext` to index "Class -> Interfaces" and "Interface -> Classes" relationships.
+- [ ] **Polymorphic Call Trace**: Update `CallGraphBuilder` to follow calls from an interface method to all known implementations in the project.
+- [ ] **Base Class Trigger Discovery**: Ensure triggers in abstract base classes are correctly attributed to their concrete subclasses.
+
+
## Phase 5: Visualization & Diagnostics
- [ ] Update `ExportService` to include trigger information.
- [ ] Update DOT/SCXML exporters to show entry points.
@@ -39,9 +46,11 @@
- [ ] Verify that inheritance in both state machine config and controllers is correctly handled.
## Phase 7: Advanced Resolution & Data Flow
-- [ ] **Complex ValueResolver**: Handle string concatenations and variable references in annotations/logic.
+- [ ] **Variable Propagation**: Track values from fields/constructors to their usage in the State Machine configuration (e.g., resolving `initial(initialState)` where `initialState` is a field).
+- [ ] **@Value Resolution**: Link `@Value` annotations to the `PropertyResolver` to substitute placeholders with literal values in the diagram.
+- [ ] **Spring Profile Support**: Update `PropertyResolver` to handle `application-{profile}.properties` and prioritize them based on an "active profiles" setting.
+- [ ] **Complex ValueResolver**: Handle string concatenations (e.g., `"PREFIX_" + MyConstants.SUFFIX`) and variable references.
- [ ] **Payload Analysis**: Extract payload types from entry points and track field usage in `sendEvent` calls.
-- [ ] **@Value Propagation**: Track property values injected into fields and constructors.
## Phase 8: Persistence & Lifecycle Mapping
- [ ] Detect persistence restoration logic (`persister.restore`).
@@ -53,5 +62,10 @@
- [ ] **Interceptors & Filters**: Detect `HandlerInterceptor` and `Filter` implementations and map them to endpoints.
## Phase 10: External Dependency Interop
-- [ ] **Library Hints**: Allow users to provide a JSON map of "Method -> Event" for third-party libraries where source is unavailable.
+- [x] **Library Hints**: Implementation complete. Supports `hints.json` in project root or `src/main/resources/`.
+ - **Format**: `[{"methodFqn": "class.method", "event": "EVENT"}]`
+ - **Purpose**: Maps external/compiled library calls to State Machine events when source code is unavailable.
- [ ] **External JAR Analysis**: (Optional) Configure JDT to scan source-attachments of dependencies if available.
+
+**Note on Library Hints**: This feature is an optional "Power User" escape hatch. The analyzer works automatically for all local source code; `hints.json` is only required to bridge the gap for third-party libraries.
+
diff --git a/settings.gradle b/settings.gradle
index 1d6131b..1d6ebc0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -12,3 +12,6 @@ include ':state_machines:inheritance_extra_functions_state_machine'
include ':state_machines:inheritance_extra_functions2_state_machine'
include ':state_machines:inheritance_extra_functions3_state_machine'
include ':state_machines:simple_state_machine'
+include ':state_machines:extended_analysis_sample'
+include ':state_machines:inheritance_sample'
+include ':state_machines:enterprise_order_system'
diff --git a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.dot b/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.dot
deleted file mode 100644
index f86efca..0000000
--- a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.dot
+++ /dev/null
@@ -1,29 +0,0 @@
-digraph statemachine {
- rankdir=LR;
- node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
- edge [fontname="Arial", fontsize=10];
-
- SUBMITTED_choice [shape=diamond, label="", fillcolor="blue", width=0.2, height=0.2];
- SUBMITTED -> SUBMITTED_choice [style=dotted, color="blue"];
- PAID_choice [shape=diamond, label="", fillcolor="green", width=0.2, height=0.2];
- PAID -> PAID_choice [style=dotted, color="green"];
- _start [shape=circle, label="", fillcolor=black, width=0.1];
- _start -> SUBMITTED;
- CANCELED [fillcolor=lightgray];
- FULFILLED [fillcolor=lightgray];
- SUBMITTED -> PAID [label="OrderEvents.PAY", style="solid", color="black"];
- PAID -> FULFILLED [label="OrderEvents.FULFILL", style="solid", color="black"];
- SUBMITTED -> CANCELED [label="OrderEvents.CANCEL [λ]", style="solid", color="black"];
- PAID -> CANCELED [label="OrderEvents.CANCEL", style="solid", color="black"];
- SUBMITTED -> SUBMITTED [label="OrderEvents.ABCD", style="solid", color="black"];
- SUBMITTED_choice -> PAID2 [label="[λ] (order=0)", style="solid", color="blue"];
- SUBMITTED_choice -> PAID3 [label="(order=1)", style="solid", color="blue"];
- PAID_choice -> PAID1 [label="[λ] (order=0)", style="solid", color="green"];
- PAID_choice -> PAID2 [label="[guard1] (order=1)", style="solid", color="green"];
- PAID_choice -> HAPPEN [label="[λ] (order=2)", style="solid", color="green"];
- PAID_choice -> PAID3 [label="(order=3)", style="solid", color="green"];
- PAID2 -> CANCELED [style="solid", color="black"];
- PAID3 -> CANCELED [style="solid", color="black"];
- PAID1 -> PAID1 [label="OrderEvents.ABCD / λ, action2", style="solid", color="black"];
-}
-
diff --git a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.scxml.xml b/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.scxml.xml
deleted file mode 100644
index 57145e3..0000000
--- a/state_machine_exporter/out_simple/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration/click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration.scxml.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java
index 76e464c..6609459 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java
@@ -16,8 +16,7 @@ public class Main {
public static void main(String[] args) {
// Manual DI / Wiring
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
- var enrichmentService = new EnrichmentService(Collections.emptyList());
- var exportService = new ExportService(exporters, enrichmentService);
+ var exportService = new ExportService(exporters);
var command = new ExporterCommand(exportService);
int exitCode = new CommandLine(command).execute(args);
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java
index 2a6fd21..c9a6b9a 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java
@@ -1,8 +1,9 @@
package click.kamil.springstatemachineexporter.analysis.enricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
+import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
public interface AnalysisEnricher {
- void enrich(AnalysisResult result, CodebaseContext context);
+ void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence);
}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java
new file mode 100644
index 0000000..ad3b361
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java
@@ -0,0 +1,28 @@
+package click.kamil.springstatemachineexporter.analysis.enricher;
+
+import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
+import click.kamil.springstatemachineexporter.analysis.model.CallChain;
+import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
+import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+@Slf4j
+public class CallChainEnricher implements AnalysisEnricher {
+
+ @Override
+ public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
+ log.info("Enriching {} with call chains", result.getName());
+
+ List chains = intelligence.findCallChains(
+ result.getMetadata().getEntryPoints(),
+ result.getMetadata().getTriggers()
+ );
+
+ result.addMetadata(CodebaseMetadata.builder()
+ .callChains(chains)
+ .build());
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/EntryPointEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/EntryPointEnricher.java
new file mode 100644
index 0000000..873cbce
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/EntryPointEnricher.java
@@ -0,0 +1,25 @@
+package click.kamil.springstatemachineexporter.analysis.enricher;
+
+import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
+import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
+import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
+import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+@Slf4j
+public class EntryPointEnricher implements AnalysisEnricher {
+
+ @Override
+ public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
+ log.info("Enriching {} with entry points", result.getName());
+
+ List entryPoints = intelligence.findEntryPoints();
+
+ result.addMetadata(CodebaseMetadata.builder()
+ .entryPoints(entryPoints)
+ .build());
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/PropertyEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/PropertyEnricher.java
new file mode 100644
index 0000000..bbae292
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/PropertyEnricher.java
@@ -0,0 +1,24 @@
+package click.kamil.springstatemachineexporter.analysis.enricher;
+
+import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
+import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
+import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+
+@Slf4j
+public class PropertyEnricher implements AnalysisEnricher {
+
+ @Override
+ public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
+ log.info("Enriching {} with properties", result.getName());
+
+ Map properties = intelligence.resolveProperties();
+
+ result.addMetadata(CodebaseMetadata.builder()
+ .properties(properties)
+ .build());
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerEnricher.java
new file mode 100644
index 0000000..44ef006
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerEnricher.java
@@ -0,0 +1,26 @@
+package click.kamil.springstatemachineexporter.analysis.enricher;
+
+import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
+import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
+import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+@Slf4j
+public class TriggerEnricher implements AnalysisEnricher {
+
+ @Override
+ public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
+ log.info("Enriching {} with triggers", result.getName());
+
+ List triggers = intelligence.findTriggerPoints();
+
+ // Initially, we just add all triggers found in the codebase to the metadata.
+ // In later steps of Phase 2, we will filter them by State Machine ID.
+ result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
+ .triggers(triggers)
+ .build());
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java
index 6a0533f..eb8fc7b 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java
@@ -1,8 +1,12 @@
package click.kamil.springstatemachineexporter.analysis.model;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import click.kamil.springstatemachineexporter.model.Transition;
import lombok.Builder;
import lombok.Getter;
+import lombok.extern.jackson.Jacksonized;
import java.util.Collections;
import java.util.List;
@@ -10,6 +14,8 @@ import java.util.Set;
@Getter
@Builder
+@Jacksonized
+@JsonIgnoreProperties(ignoreUnknown = true)
public class AnalysisResult {
private final String name;
private final List transitions;
@@ -23,18 +29,22 @@ public class AnalysisResult {
public void addMetadata(CodebaseMetadata newMetadata) {
if (newMetadata == null) return;
- List combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers());
- combinedTriggers.addAll(newMetadata.getTriggers());
+ List combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers());
+ if (newMetadata.getTriggers() != null) combinedTriggers.addAll(newMetadata.getTriggers());
- List combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints());
- combinedEntryPoints.addAll(newMetadata.getEntryPoints());
+ List combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints());
+ if (newMetadata.getEntryPoints() != null) combinedEntryPoints.addAll(newMetadata.getEntryPoints());
+
+ List combinedCallChains = new java.util.ArrayList<>(this.metadata.getCallChains());
+ if (newMetadata.getCallChains() != null) combinedCallChains.addAll(newMetadata.getCallChains());
java.util.Map combinedProperties = new java.util.HashMap<>(this.metadata.getProperties());
- combinedProperties.putAll(newMetadata.getProperties());
+ if (newMetadata.getProperties() != null) combinedProperties.putAll(newMetadata.getProperties());
this.metadata = CodebaseMetadata.builder()
.triggers(Collections.unmodifiableList(combinedTriggers))
.entryPoints(Collections.unmodifiableList(combinedEntryPoints))
+ .callChains(Collections.unmodifiableList(combinedCallChains))
.properties(Collections.unmodifiableMap(combinedProperties))
.build();
}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CallChain.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CallChain.java
new file mode 100644
index 0000000..0f3fbed
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CallChain.java
@@ -0,0 +1,18 @@
+package click.kamil.springstatemachineexporter.analysis.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+import java.util.List;
+
+@Data
+@Builder
+@Jacksonized
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CallChain {
+ private final EntryPoint entryPoint;
+ private final List methodChain; // e.g., ["Controller.submit", "Service.process", "Service.send"]
+ private final TriggerPoint triggerPoint;
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java
index 08bcabf..a0726a5 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java
@@ -1,7 +1,9 @@
package click.kamil.springstatemachineexporter.analysis.model;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Builder;
import lombok.Getter;
+import lombok.extern.jackson.Jacksonized;
import java.util.ArrayList;
import java.util.Collections;
@@ -10,19 +12,19 @@ import java.util.Map;
@Getter
@Builder(toBuilder = true)
+@Jacksonized
+@JsonIgnoreProperties(ignoreUnknown = true)
public class CodebaseMetadata {
- private final List triggers;
- private final List entryPoints;
- private final Map properties;
+ @Builder.Default
+ private final List triggers = Collections.emptyList();
+ @Builder.Default
+ private final List entryPoints = Collections.emptyList();
+ @Builder.Default
+ private final List callChains = Collections.emptyList();
+ @Builder.Default
+ private final Map properties = Collections.emptyMap();
public static CodebaseMetadata empty() {
- return CodebaseMetadata.builder()
- .triggers(Collections.emptyList())
- .entryPoints(Collections.emptyList())
- .properties(Collections.emptyMap())
- .build();
+ return CodebaseMetadata.builder().build();
}
-
- public record TriggerPoint(String className, String methodName, String event, Map metadata) {}
- public record EntryPoint(String className, String methodName, String type, Map metadata) {}
}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java
new file mode 100644
index 0000000..29ca462
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/EntryPoint.java
@@ -0,0 +1,22 @@
+package click.kamil.springstatemachineexporter.analysis.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+import java.util.Map;
+
+@Data
+@Builder
+@Jacksonized
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EntryPoint {
+ public enum Type { REST, KAFKA, RABBIT, JMS, CUSTOM }
+
+ private final Type type;
+ private final String name; // e.g., "POST /orders" or "Kafka Topic: orders"
+ private final String className;
+ private final String methodName;
+ private final Map metadata; // e.g., path, topic, exchange
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/LibraryHint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/LibraryHint.java
new file mode 100644
index 0000000..34b787a
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/LibraryHint.java
@@ -0,0 +1,17 @@
+package click.kamil.springstatemachineexporter.analysis.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+import java.util.List;
+
+@Data
+@Builder
+@Jacksonized
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class LibraryHint {
+ private final String methodFqn; // e.g., "com.thirdparty.Workflow.send"
+ private final String event; // The event it triggers
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java
new file mode 100644
index 0000000..0e6f726
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java
@@ -0,0 +1,21 @@
+package click.kamil.springstatemachineexporter.analysis.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.jackson.Jacksonized;
+
+import java.util.Map;
+
+@Data
+@Builder
+@Jacksonized
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class TriggerPoint {
+ private final String event;
+ private final String className;
+ private final String methodName;
+ private final String sourceModule;
+ private final String stateMachineId; // Optional: to link to a specific SM instance
+ private final int lineNumber;
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java
new file mode 100644
index 0000000..8adbe48
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java
@@ -0,0 +1,92 @@
+package click.kamil.springstatemachineexporter.analysis.resolver;
+
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jdt.core.dom.*;
+
+@Slf4j
+public class ConstantResolver {
+
+ public String resolve(Expression expr, CodebaseContext context) {
+ if (expr == null) return null;
+
+ // 1. Literal?
+ if (expr instanceof StringLiteral sl) {
+ return sl.getLiteralValue();
+ }
+ if (expr instanceof NumberLiteral nl) {
+ return nl.getToken();
+ }
+ if (expr instanceof BooleanLiteral bl) {
+ return String.valueOf(bl.booleanValue());
+ }
+
+ // 2. JDT Bindings? (Most robust)
+ IVariableBinding binding = null;
+ if (expr instanceof SimpleName sn) {
+ IBinding b = sn.resolveBinding();
+ if (b instanceof IVariableBinding vb) binding = vb;
+ } else if (expr instanceof QualifiedName qn) {
+ IBinding b = qn.resolveBinding();
+ if (b instanceof IVariableBinding vb) binding = vb;
+ }
+
+ if (binding != null && binding.isField()) {
+ Object value = binding.getConstantValue();
+ if (value != null) return value.toString();
+ }
+
+ // 3. Fallback: Manual AST Traversal (if bindings not available or resolved)
+ if (expr instanceof InfixExpression infix) {
+ return resolveInfix(infix, context);
+ }
+ if (expr instanceof QualifiedName qn) {
+ return resolveManual(qn, context);
+ }
+
+ return null;
+ }
+
+ private String resolveInfix(InfixExpression expr, CodebaseContext context) {
+ if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
+
+ String left = resolve(expr.getLeftOperand(), context);
+ String right = resolve(expr.getRightOperand(), context);
+
+ if (left == null || right == null) return null;
+
+ StringBuilder sb = new StringBuilder(left + right);
+ if (expr.hasExtendedOperands()) {
+ for (Object operand : expr.extendedOperands()) {
+ String val = resolve((Expression) operand, context);
+ if (val == null) return null;
+ sb.append(val);
+ }
+ }
+ return sb.toString();
+ }
+
+ private String resolveManual(QualifiedName qn, CodebaseContext context) {
+ String qualifier = qn.getQualifier().toString();
+ String name = qn.getName().getIdentifier();
+
+ CompilationUnit contextCu = (CompilationUnit) qn.getRoot();
+ TypeDeclaration td = context.getTypeDeclaration(qualifier, contextCu);
+
+ if (td != null) {
+ for (FieldDeclaration fd : td.getFields()) {
+ for (Object fragmentObj : fd.fragments()) {
+ if (fragmentObj instanceof VariableDeclarationFragment fragment) {
+ if (fragment.getName().getIdentifier().equals(name)) {
+ Expression initializer = fragment.getInitializer();
+ // Simple recursive resolve (be careful of cycles)
+ if (initializer instanceof StringLiteral sl) return sl.getLiteralValue();
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java
new file mode 100644
index 0000000..992bcb3
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/PropertyResolver.java
@@ -0,0 +1,95 @@
+package click.kamil.springstatemachineexporter.analysis.resolver;
+
+import lombok.extern.slf4j.Slf4j;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Stream;
+
+@Slf4j
+public class PropertyResolver {
+
+ private final List activeProfiles = new ArrayList<>();
+
+ public void setActiveProfiles(List profiles) {
+ this.activeProfiles.clear();
+ if (profiles != null) {
+ this.activeProfiles.addAll(profiles);
+ }
+ }
+
+ public Map resolveProperties(Path rootDir) {
+ Map allProperties = new HashMap<>();
+
+ // 1. Load base properties
+ loadMatching(rootDir, "application.properties", allProperties);
+ loadMatching(rootDir, "application.yml", allProperties);
+ loadMatching(rootDir, "application.yaml", allProperties);
+
+ // 2. Load profile-specific properties (overriding base)
+ for (String profile : activeProfiles) {
+ loadMatching(rootDir, "application-" + profile + ".properties", allProperties);
+ loadMatching(rootDir, "application-" + profile + ".yml", allProperties);
+ loadMatching(rootDir, "application-" + profile + ".yaml", allProperties);
+ }
+
+ return allProperties;
+ }
+
+ private void loadMatching(Path rootDir, String fileName, Map target) {
+ try (Stream paths = Files.walk(rootDir)) {
+ paths.filter(p -> Files.isRegularFile(p))
+ .filter(p -> p.getFileName().toString().equals(fileName))
+ .forEach(p -> {
+ if (p.toString().endsWith(".properties")) {
+ target.putAll(loadProperties(p));
+ } else {
+ target.putAll(loadYaml(p));
+ }
+ });
+ } catch (IOException e) {
+ log.error("Failed to scan for {} in {}", fileName, rootDir, e);
+ }
+ }
+
+ private Map loadProperties(Path path) {
+ Map props = new HashMap<>();
+ Properties p = new Properties();
+ try (InputStream is = Files.newInputStream(path)) {
+ p.load(is);
+ p.forEach((k, v) -> props.put(k.toString(), v.toString()));
+ } catch (IOException e) {
+ log.warn("Failed to load properties from {}", path);
+ }
+ return props;
+ }
+
+ private Map loadYaml(Path path) {
+ // Placeholder for future YAML support
+ log.warn("YAML parsing not fully implemented yet for {}", path);
+ return new HashMap<>();
+ }
+
+ public String resolveValue(String placeholder, Map properties) {
+ if (placeholder == null || !placeholder.contains("${")) return placeholder;
+
+ // 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);
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java
new file mode 100644
index 0000000..15cb7b7
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java
@@ -0,0 +1,159 @@
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jdt.core.dom.*;
+
+import java.util.*;
+
+@Slf4j
+@RequiredArgsConstructor
+public class CallGraphBuilder {
+ private final CodebaseContext context;
+
+ public List findChains(List entryPoints, List triggers) {
+ Map> callGraph = buildCallGraph();
+ List chains = new ArrayList<>();
+
+ for (EntryPoint ep : entryPoints) {
+ String startMethod = ep.getClassName() + "." + ep.getMethodName();
+ for (TriggerPoint tp : triggers) {
+ String targetMethod = tp.getClassName() + "." + tp.getMethodName();
+ List path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
+ if (path != null) {
+ chains.add(CallChain.builder()
+ .entryPoint(ep)
+ .triggerPoint(tp)
+ .methodChain(path)
+ .build());
+ }
+ }
+ }
+ return chains;
+ }
+
+ private Map> buildCallGraph() {
+ Map> graph = new HashMap<>();
+ for (CompilationUnit cu : context.getCompilationUnits()) {
+ cu.accept(new ASTVisitor() {
+ private String currentMethodFqn;
+
+ @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 calledMethods = resolveCalledMethodsPolymorphic(node);
+ for (String calledMethod : calledMethods) {
+ graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(calledMethod);
+ }
+ }
+ return super.visit(node);
+ }
+ });
+ }
+ return graph;
+ }
+
+ private List resolveCalledMethodsPolymorphic(MethodInvocation node) {
+ String baseCalled = resolveCalledMethod(node);
+ if (baseCalled == null) return Collections.emptyList();
+
+ List allResolved = new ArrayList<>();
+ allResolved.add(baseCalled);
+
+ // Polymorphic fan-out
+ if (baseCalled.contains(".")) {
+ String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
+ String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
+
+ List 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) {
+ // Local call or static import (simplified)
+ TypeDeclaration td = findEnclosingType(node);
+ return td != null ? context.getFqn(td) + "." + methodName : null;
+ }
+
+ // Try to resolve receiver type
+ // This is a simplified resolution without full JDT bindings.
+ // It works for local variables and fields within the same project.
+ ITypeBinding binding = receiver.resolveTypeBinding();
+ if (binding != null) {
+ return binding.getQualifiedName() + "." + methodName;
+ }
+
+ // Fallback: heuristic resolution for simple cases like 'orderService.process()'
+ if (receiver instanceof SimpleName sn) {
+ String receiverName = sn.getIdentifier();
+ // Look for fields or variables (simplified)
+ // For now, we'll return a "best guess" if we can't resolve it perfectly
+ // In a real implementation, we'd use JDT bindings more heavily.
+ return receiverName + "." + methodName;
+ }
+
+ return null;
+ }
+
+ private List findPath(String start, String target, Map> graph, Set visited) {
+ if (start.equals(target)) return new ArrayList<>(List.of(start));
+ if (!visited.add(start)) return null;
+
+ List neighbors = graph.get(start);
+ if (neighbors != null) {
+ for (String neighbor : neighbors) {
+ // Heuristic: neighbor might be 'orderService.process' while target is 'click.kamil...OrderService.process'
+ if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
+ return new ArrayList<>(List.of(start, target));
+ }
+ List path = findPath(neighbor, target, graph, visited);
+ if (path != null) {
+ path.add(0, start);
+ return path;
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean isHeuristicMatch(String neighbor, String target) {
+ // Match 'orderService.process' with 'click.kamil.OrderService.process'
+ if (target.endsWith("." + neighbor)) return true;
+ // Match simple names if we don't have FQN
+ String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
+ String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
+
+ // Very basic heuristic for now
+ return neighbor.toLowerCase().contains(simpleTarget.toLowerCase()) || target.toLowerCase().contains(simpleNeighbor.toLowerCase());
+ }
+
+ private TypeDeclaration findEnclosingType(ASTNode node) {
+ ASTNode parent = node.getParent();
+ while (parent != null && !(parent instanceof TypeDeclaration)) {
+ parent = parent.getParent();
+ }
+ return (TypeDeclaration) parent;
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CodebaseIntelligenceProvider.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CodebaseIntelligenceProvider.java
new file mode 100644
index 0000000..8055a73
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CodebaseIntelligenceProvider.java
@@ -0,0 +1,30 @@
+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;
+import java.util.Map;
+
+public interface CodebaseIntelligenceProvider {
+ /**
+ * Discovers all locations where events are triggered.
+ */
+ List findTriggerPoints();
+
+ /**
+ * Discovers all entry points (REST, Kafka, etc.).
+ */
+ List findEntryPoints();
+
+ /**
+ * Links Entry Points to Trigger Points.
+ */
+ List findCallChains(List entryPoints, List triggers);
+
+ /**
+ * Resolves configuration properties.
+ */
+ Map resolveProperties();
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java
index 222feb8..bb8a4b5 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java
@@ -11,9 +11,9 @@ import java.util.List;
public class EnrichmentService {
private final List enrichers;
- public void enrich(AnalysisResult result, CodebaseContext context) {
+ public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
for (AnalysisEnricher enricher : enrichers) {
- enricher.enrich(result, context);
+ enricher.enrich(result, context, intelligence);
}
}
}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java
new file mode 100644
index 0000000..6226f94
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java
@@ -0,0 +1,147 @@
+package click.kamil.springstatemachineexporter.analysis.service;
+
+import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
+import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
+import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jdt.core.dom.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+public class GenericEventDetector {
+ private final CodebaseContext context;
+ private final ConstantResolver constantResolver;
+ private final List hints;
+
+ public List detect(CompilationUnit cu) {
+ List triggers = new ArrayList<>();
+ String fileName = cu.getJavaElement() != null ? cu.getJavaElement().getElementName() : "unknown";
+ log.debug("Scanning for triggers in {}", fileName);
+
+ cu.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(MethodInvocation node) {
+ String methodName = node.getName().getIdentifier();
+
+ // 1. Core sendEvent
+ if ("sendEvent".equals(methodName)) {
+ processSendEvent(node, cu, triggers);
+ }
+
+ // 2. Library Hints
+ processHints(node, cu, triggers);
+
+ return super.visit(node);
+ }
+ });
+ return triggers;
+ }
+
+ private void processSendEvent(MethodInvocation node, CompilationUnit cu, List triggers) {
+ log.info("Found potential trigger: {}", node);
+ TriggerPoint trigger = buildTriggerPoint(node, cu, null);
+ if (trigger != null) {
+ log.info("Successfully built trigger point: {}", trigger.getEvent());
+ triggers.add(trigger);
+ }
+ }
+
+ private void processHints(MethodInvocation node, CompilationUnit cu, List triggers) {
+ if (hints == null || hints.isEmpty()) return;
+
+ String calledMethod = resolveCalledMethodName(node);
+ if (calledMethod == null) return;
+
+ log.info("Checking method call '{}' against {} hints", calledMethod, hints.size());
+
+ for (LibraryHint hint : hints) {
+ if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
+ log.info("MATCH! Found hint for '{}'", calledMethod);
+ TriggerPoint trigger = buildTriggerPoint(node, cu, hint.getEvent());
+ if (trigger != null) {
+ log.info("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
+ triggers.add(trigger);
+ }
+ }
+ }
+ }
+
+ private boolean isHintMatch(String called, String hintFqn) {
+ return hintFqn.endsWith("." + called);
+ }
+
+ private String resolveCalledMethodName(MethodInvocation node) {
+ Expression receiver = node.getExpression();
+ String methodName = node.getName().getIdentifier();
+ if (receiver == null) return methodName;
+
+ if (receiver instanceof SimpleName sn) {
+ String name = sn.getIdentifier();
+ // Check if it's a field in the enclosing class
+ TypeDeclaration td = findEnclosingType(node);
+ if (td != null) {
+ for (FieldDeclaration fd : td.getFields()) {
+ for (Object fragObj : fd.fragments()) {
+ if (fragObj instanceof VariableDeclarationFragment fragment) {
+ if (fragment.getName().getIdentifier().equals(name)) {
+ String typeName = fd.getType().toString();
+ return typeName + "." + methodName;
+ }
+ }
+ }
+ }
+ }
+ return name + "." + methodName;
+ }
+
+ return receiver.toString() + "." + methodName;
+ }
+
+ private TriggerPoint buildTriggerPoint(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
+ String eventValue;
+ if (forcedEvent != null) {
+ eventValue = forcedEvent;
+ } else {
+ if (node.arguments().isEmpty()) return null;
+ Expression eventExpr = (Expression) node.arguments().get(0);
+ eventValue = constantResolver.resolve(eventExpr, context);
+ if (eventValue == null) {
+ eventValue = eventExpr.toString();
+ }
+ }
+
+ MethodDeclaration method = findEnclosingMethod(node);
+ TypeDeclaration type = findEnclosingType(node);
+
+ if (type == null) return null;
+
+ return TriggerPoint.builder()
+ .event(eventValue)
+ .className(context.getFqn(type))
+ .methodName(method != null ? method.getName().getIdentifier() : "initializer")
+ .lineNumber(cu.getLineNumber(node.getStartPosition()))
+ .build();
+ }
+
+ private MethodDeclaration findEnclosingMethod(ASTNode node) {
+ ASTNode parent = node.getParent();
+ while (parent != null && !(parent instanceof MethodDeclaration)) {
+ parent = parent.getParent();
+ }
+ return (MethodDeclaration) parent;
+ }
+
+ private TypeDeclaration findEnclosingType(ASTNode node) {
+ ASTNode parent = node.getParent();
+ while (parent != null && !(parent instanceof TypeDeclaration)) {
+ parent = parent.getParent();
+ }
+ return (TypeDeclaration) parent;
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/InterceptorDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/InterceptorDetector.java
new file mode 100644
index 0000000..52172ef
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/InterceptorDetector.java
@@ -0,0 +1,75 @@
+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.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RequiredArgsConstructor
+public class InterceptorDetector {
+ private final CodebaseContext context;
+
+ public List detect(CompilationUnit cu) {
+ List entryPoints = new ArrayList<>();
+ cu.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(TypeDeclaration node) {
+ if (isInterceptor(node)) {
+ processInterceptor(node, entryPoints);
+ }
+ return super.visit(node);
+ }
+ });
+ return entryPoints;
+ }
+
+ private boolean isInterceptor(TypeDeclaration node) {
+ if (node.getSuperclassType() != null) {
+ // Check for common adapter/base classes if needed
+ }
+ for (Object interfaceType : node.superInterfaceTypes()) {
+ String name = interfaceType.toString();
+ if (name.contains("HandlerInterceptor") || name.contains("Filter") || name.contains("WebRequestInterceptor")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void processInterceptor(TypeDeclaration node, List entryPoints) {
+ String fqn = context.getFqn(node);
+ for (MethodDeclaration method : node.getMethods()) {
+ String methodName = method.getName().getIdentifier();
+ if (isInterceptorMethod(methodName)) {
+ Map metadata = new HashMap<>();
+ metadata.put("interceptorType", getInterceptorType(node));
+
+ entryPoints.add(EntryPoint.builder()
+ .type(EntryPoint.Type.CUSTOM)
+ .name("INTERCEPTOR: " + node.getName().getIdentifier() + "." + methodName)
+ .className(fqn)
+ .methodName(methodName)
+ .metadata(metadata)
+ .build());
+ }
+ }
+ }
+
+ private boolean isInterceptorMethod(String name) {
+ return name.equals("preHandle") || name.equals("postHandle") || name.equals("afterCompletion") || name.equals("doFilter");
+ }
+
+ private String getInterceptorType(TypeDeclaration node) {
+ for (Object interfaceType : node.superInterfaceTypes()) {
+ String name = interfaceType.toString();
+ if (name.contains("HandlerInterceptor")) return "Spring MVC Interceptor";
+ if (name.contains("Filter")) return "Servlet Filter";
+ }
+ return "Generic Interceptor";
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java
new file mode 100644
index 0000000..5ede38b
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtIntelligenceProvider.java
@@ -0,0 +1,74 @@
+package click.kamil.springstatemachineexporter.analysis.service;
+
+import click.kamil.springstatemachineexporter.analysis.model.CallChain;
+import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
+import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
+import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
+import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
+ private final CodebaseContext context;
+ private final Path rootDir;
+ private final ConstantResolver constantResolver = new ConstantResolver();
+ private final GenericEventDetector eventDetector;
+ private final SpringMvcDetector mvcDetector;
+ private final CallGraphBuilder callGraphBuilder;
+ private final LifecycleDetector lifecycleDetector;
+ private final InterceptorDetector interceptorDetector;
+
+ public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
+ this.context = context;
+ this.rootDir = rootDir;
+ this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
+ this.mvcDetector = new SpringMvcDetector(context, constantResolver);
+ this.callGraphBuilder = new CallGraphBuilder(context);
+ this.lifecycleDetector = new LifecycleDetector(context);
+ this.interceptorDetector = new InterceptorDetector(context);
+ }
+
+ @Override
+ public List findTriggerPoints() {
+ List allTriggers = new java.util.ArrayList<>();
+ var cus = context.getCompilationUnits();
+ log.info("JdtIntelligenceProvider scanning {} compilation units for triggers", cus.size());
+ for (CompilationUnit cu : cus) {
+ allTriggers.addAll(eventDetector.detect(cu));
+ allTriggers.addAll(lifecycleDetector.detect(cu));
+ }
+ log.info("Found {} triggers (including lifecycle) in total", allTriggers.size());
+ return allTriggers;
+ }
+
+ @Override
+ public List findEntryPoints() {
+ List allEntryPoints = new java.util.ArrayList<>();
+ var cus = context.getCompilationUnits();
+ log.info("JdtIntelligenceProvider scanning {} compilation units for entry points", cus.size());
+ for (CompilationUnit cu : cus) {
+ allEntryPoints.addAll(mvcDetector.detect(cu));
+ allEntryPoints.addAll(interceptorDetector.detect(cu));
+ }
+ log.info("Found {} entry points (including interceptors) in total", allEntryPoints.size());
+ return allEntryPoints;
+ }
+
+ @Override
+ public List findCallChains(List entryPoints, List triggers) {
+ log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
+ return callGraphBuilder.findChains(entryPoints, triggers);
+ }
+
+ @Override
+ public Map resolveProperties() {
+ return context.getProperties();
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java
new file mode 100644
index 0000000..778470b
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/LifecycleDetector.java
@@ -0,0 +1,62 @@
+package click.kamil.springstatemachineexporter.analysis.service;
+
+import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.RequiredArgsConstructor;
+import org.eclipse.jdt.core.dom.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiredArgsConstructor
+public class LifecycleDetector {
+ private final CodebaseContext context;
+
+ public List detect(CompilationUnit cu) {
+ List lifecyclePoints = new ArrayList<>();
+ cu.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(MethodInvocation node) {
+ String methodName = node.getName().getIdentifier();
+ if ("restore".equals(methodName) || "reset".equals(methodName)) {
+ TriggerPoint point = buildLifecyclePoint(node, cu, methodName);
+ if (point != null) {
+ lifecyclePoints.add(point);
+ }
+ }
+ return super.visit(node);
+ }
+ });
+ return lifecyclePoints;
+ }
+
+ private TriggerPoint buildLifecyclePoint(MethodInvocation node, CompilationUnit cu, String type) {
+ MethodDeclaration method = findEnclosingMethod(node);
+ TypeDeclaration typeDecl = findEnclosingType(node);
+
+ if (typeDecl == null) return null;
+
+ return TriggerPoint.builder()
+ .event("[LIFECYCLE:" + type.toUpperCase() + "]")
+ .className(context.getFqn(typeDecl))
+ .methodName(method != null ? method.getName().getIdentifier() : "initializer")
+ .lineNumber(cu.getLineNumber(node.getStartPosition()))
+ .build();
+ }
+
+ private MethodDeclaration findEnclosingMethod(ASTNode node) {
+ ASTNode parent = node.getParent();
+ while (parent != null && !(parent instanceof MethodDeclaration)) {
+ parent = parent.getParent();
+ }
+ return (MethodDeclaration) parent;
+ }
+
+ private TypeDeclaration findEnclosingType(ASTNode node) {
+ ASTNode parent = node.getParent();
+ while (parent != null && !(parent instanceof TypeDeclaration)) {
+ parent = parent.getParent();
+ }
+ return (TypeDeclaration) parent;
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java
new file mode 100644
index 0000000..86f6452
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/SpringMvcDetector.java
@@ -0,0 +1,179 @@
+package click.kamil.springstatemachineexporter.analysis.service;
+
+import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
+import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jdt.core.dom.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RequiredArgsConstructor
+public class SpringMvcDetector {
+ private final CodebaseContext context;
+ private final ConstantResolver constantResolver;
+
+ public List detect(CompilationUnit cu) {
+ List entryPoints = new ArrayList<>();
+ cu.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(TypeDeclaration node) {
+ if (isRestController(node)) {
+ processController(node, entryPoints);
+ }
+ return super.visit(node);
+ }
+ });
+ return entryPoints;
+ }
+
+ private boolean isRestController(TypeDeclaration node) {
+ for (Object modifier : node.modifiers()) {
+ if (modifier instanceof Annotation annotation) {
+ String name = annotation.getTypeName().getFullyQualifiedName();
+ if (name.endsWith("RestController") || name.endsWith("Controller")) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private void processController(TypeDeclaration controller, List entryPoints) {
+ String basePath = getMappingPathHierarchical(controller);
+ for (MethodDeclaration method : controller.getMethods()) {
+ EntryPoint ep = processMethodHierarchical(method, basePath, controller);
+ if (ep != null) {
+ entryPoints.add(ep);
+ }
+ }
+ }
+
+ private String getMappingPathHierarchical(TypeDeclaration td) {
+ // 1. Check class itself
+ String path = getMappingPath(td);
+ if (!path.isEmpty()) return path;
+
+ // 2. Check interfaces
+ for (Object interfaceType : td.superInterfaceTypes()) {
+ TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString());
+ if (itd != null) {
+ path = getMappingPath(itd);
+ if (!path.isEmpty()) return path;
+ }
+ }
+
+ // 3. Check superclass
+ if (td.getSuperclassType() != null) {
+ TypeDeclaration std = context.getTypeDeclaration(td.getSuperclassType().toString());
+ if (std != null) {
+ return getMappingPathHierarchical(std);
+ }
+ }
+
+ return "";
+ }
+
+ private EntryPoint processMethodHierarchical(MethodDeclaration method, String basePath, TypeDeclaration controller) {
+ // 1. Check the method itself
+ EntryPoint ep = processMethod(method, basePath, controller);
+ if (ep != null) return ep;
+
+ // 2. Check interface methods
+ String methodName = method.getName().getIdentifier();
+ for (Object interfaceType : controller.superInterfaceTypes()) {
+ TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString());
+ if (itd != null) {
+ for (MethodDeclaration im : itd.getMethods()) {
+ if (im.getName().getIdentifier().equals(methodName)) {
+ ep = processMethod(im, basePath, controller);
+ if (ep != null) return ep;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private EntryPoint processMethod(MethodDeclaration method, String basePath, TypeDeclaration controller) {
+ for (Object modifier : method.modifiers()) {
+ if (modifier instanceof Annotation annotation) {
+ String name = annotation.getTypeName().getFullyQualifiedName();
+ String verb = getHttpVerb(name);
+ if (verb != null) {
+ String methodPath = getMappingPath(annotation);
+ String fullPath = combinePaths(basePath, methodPath);
+
+ Map metadata = new HashMap<>();
+ metadata.put("path", fullPath);
+ metadata.put("verb", verb);
+
+ return EntryPoint.builder()
+ .type(EntryPoint.Type.REST)
+ .name(verb + " " + fullPath)
+ .className(context.getFqn(controller))
+ .methodName(method.getName().getIdentifier())
+ .metadata(metadata)
+ .build();
+ }
+ }
+ }
+ return null;
+ }
+
+ private String getHttpVerb(String annotationName) {
+ if (annotationName.endsWith("PostMapping")) return "POST";
+ if (annotationName.endsWith("GetMapping")) return "GET";
+ if (annotationName.endsWith("PutMapping")) return "PUT";
+ if (annotationName.endsWith("DeleteMapping")) return "DELETE";
+ if (annotationName.endsWith("PatchMapping")) return "PATCH";
+ if (annotationName.endsWith("RequestMapping")) return "REQUEST";
+ return null;
+ }
+
+ private String getMappingPath(TypeDeclaration td) {
+ for (Object modifier : td.modifiers()) {
+ if (modifier instanceof Annotation annotation) {
+ if (annotation.getTypeName().getFullyQualifiedName().endsWith("RequestMapping")) {
+ return getMappingPath(annotation);
+ }
+ }
+ }
+ return "";
+ }
+
+ private String getMappingPath(Annotation annotation) {
+ Expression value = null;
+ if (annotation instanceof SingleMemberAnnotation sma) {
+ value = sma.getValue();
+ } else if (annotation instanceof NormalAnnotation na) {
+ for (Object pairObj : na.values()) {
+ MemberValuePair pair = (MemberValuePair) pairObj;
+ if ("value".equals(pair.getName().getIdentifier()) || "path".equals(pair.getName().getIdentifier())) {
+ value = pair.getValue();
+ break;
+ }
+ }
+ }
+
+ if (value != null) {
+ String resolved = constantResolver.resolve(value, context);
+ return resolved != null ? resolved : value.toString();
+ }
+ return "";
+ }
+
+ private String combinePaths(String base, String method) {
+ String b = base.startsWith("/") ? base : "/" + base;
+ if (b.endsWith("/")) b = b.substring(0, b.length() - 1);
+ String m = method.startsWith("/") ? method : "/" + method;
+ if ("/".equals(m)) m = "";
+ return b + m;
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java
index 21c8546..a1a459a 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/app/AstTransitionParser.java
@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter.ast.app;
+import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.model.Action;
import click.kamil.springstatemachineexporter.model.Guard;
import click.kamil.springstatemachineexporter.model.State;
@@ -26,6 +27,7 @@ import java.util.Set;
import java.util.stream.Collectors;
public class AstTransitionParser {
+ private static final ConstantResolver constantResolver = new ConstantResolver();
private static final Set SUPPORTED_TRANSITION_METHOD_NAMES = Arrays.stream(TransitionType.values())
.map(TransitionType::getMethodName)
@@ -215,7 +217,12 @@ public class AstTransitionParser {
});
case "event" -> {
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
- t.setEvent(QuotedExpression.of(resolved).toStringWithoutQuotes());
+ String eventValue = constantResolver.resolve(resolved, context);
+ if (eventValue != null) {
+ t.setEvent(eventValue);
+ } else {
+ t.setEvent(QuotedExpression.of(resolved).toStringWithoutQuotes());
+ }
}
case "guard" -> {
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java
index 5508593..37dcbf6 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java
@@ -1,24 +1,19 @@
package click.kamil.springstatemachineexporter.ast.common;
+import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
+import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
+import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
import click.kamil.springstatemachineexporter.model.State;
-import org.eclipse.jdt.core.dom.AST;
-import org.eclipse.jdt.core.dom.ASTParser;
-import org.eclipse.jdt.core.dom.Annotation;
-import org.eclipse.jdt.core.dom.CompilationUnit;
-import org.eclipse.jdt.core.dom.Expression;
-import org.eclipse.jdt.core.dom.ImportDeclaration;
-import org.eclipse.jdt.core.dom.MethodDeclaration;
-import org.eclipse.jdt.core.dom.Modifier;
-import org.eclipse.jdt.core.dom.QualifiedName;
-import org.eclipse.jdt.core.dom.SimpleName;
-import org.eclipse.jdt.core.dom.StringLiteral;
-import org.eclipse.jdt.core.dom.Type;
-import org.eclipse.jdt.core.dom.TypeDeclaration;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.eclipse.jdt.core.dom.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -30,36 +25,135 @@ public class CodebaseContext {
private final Map classPaths = new HashMap<>();
private final Map simpleNameToFqn = new HashMap<>();
private final Set ambiguousSimpleNames = new HashSet<>();
+ private final ConstantResolver constantResolver = new ConstantResolver();
+ private final PropertyResolver propertyResolver = new PropertyResolver();
+ private Map properties = new HashMap<>();
+ private List libraryHints = new ArrayList<>();
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public void loadLibraryHints(Path hintsFile) throws IOException {
+ if (Files.exists(hintsFile)) {
+ System.out.println("Loading hints from " + hintsFile.toAbsolutePath());
+ this.libraryHints = objectMapper.readValue(hintsFile.toFile(), new TypeReference>() {});
+ System.out.println("Loaded " + libraryHints.size() + " library hints");
+ }
+ }
+
+ public List getLibraryHints() {
+ return Collections.unmodifiableList(libraryHints);
+ }
+
+ private final Set ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/**", "**/node_modules/**", "**/out/**"));
+ private String[] classpath = new String[0];
+ private String[] sourcepath = new String[0];
+ private boolean resolveBindings = false;
+
+ public Map getProperties() {
+ return Collections.unmodifiableMap(properties);
+ }
+
+ public void setClasspath(List classpath) {
+ this.classpath = classpath.toArray(new String[0]);
+ }
+
+ public void setSourcepath(List sourcepath) {
+ this.sourcepath = sourcepath.toArray(new String[0]);
+ }
+
+ public void setResolveBindings(boolean resolveBindings) {
+ this.resolveBindings = resolveBindings;
+ }
+
+ public void setActiveProfiles(List profiles) {
+ this.propertyResolver.setActiveProfiles(profiles);
+ }
public void scan(Path rootDir) throws IOException {
- List javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java");
+ scan(rootDir, Collections.emptySet());
+ }
+
+ private final List allCus = new ArrayList<>();
+
+ private final Map> interfaceToImpls = new HashMap<>();
+
+ public void scan(Path rootDir, Set customIgnorePatterns) throws IOException {
+ this.properties = propertyResolver.resolveProperties(rootDir);
+
+ Set activeIgnore = new HashSet<>(ignorePatterns);
+ activeIgnore.addAll(customIgnorePatterns);
+
+ List javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java", activeIgnore);
for (Path javaFile : javaFiles) {
String source = Files.readString(javaFile);
- CompilationUnit cu = parse(source);
+ CompilationUnit cu = parse(source, javaFile.getFileName().toString());
+ allCus.add(cu);
+
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
for (Object type : cu.types()) {
if (type instanceof TypeDeclaration td) {
- String simpleName = td.getName().getIdentifier();
- String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
- classes.put(fqn, cu);
- classPaths.put(fqn, javaFile);
-
- if (simpleNameToFqn.containsKey(simpleName)) {
- String existingFqn = simpleNameToFqn.get(simpleName);
- if (!existingFqn.equals(fqn)) {
- ambiguousSimpleNames.add(simpleName);
- }
- } else {
- simpleNameToFqn.put(simpleName, fqn);
- }
+ indexType(td, packageName, javaFile);
}
}
}
}
+ private void indexType(TypeDeclaration td, String packageName, Path javaFile) {
+ String simpleName = td.getName().getIdentifier();
+ String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
+
+ classes.put(fqn, (CompilationUnit) td.getRoot());
+ classPaths.put(fqn, javaFile);
+
+ if (simpleNameToFqn.containsKey(simpleName)) {
+ String existingFqn = simpleNameToFqn.get(simpleName);
+ if (!existingFqn.equals(fqn)) {
+ ambiguousSimpleNames.add(simpleName);
+ }
+ } else {
+ simpleNameToFqn.put(simpleName, fqn);
+ }
+
+ // Inheritance Mapping
+ for (Object itf : td.superInterfaceTypes()) {
+ String itfName = itf.toString();
+ interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
+ }
+ }
+
+ public List getImplementations(String interfaceName) {
+ // Try direct match
+ List impls = interfaceToImpls.get(interfaceName);
+ if (impls != null) return impls;
+
+ // Try FQN match if input was simple name
+ String fqn = simpleNameToFqn.get(interfaceName);
+ if (fqn != null) return interfaceToImpls.getOrDefault(fqn, Collections.emptyList());
+
+ return Collections.emptyList();
+ }
+
public State resolveState(Expression expr, CompilationUnit cu) {
if (expr == null)
return null;
+
+ // 1. Check for constants
+ String resolvedValue = constantResolver.resolve(expr, this);
+ if (resolvedValue != null) {
+ return State.of(expr.toString(), resolvedValue);
+ }
+
+ // 2. Check for @Value fields (SimpleName)
+ if (expr instanceof SimpleName sn) {
+ String fieldName = sn.getIdentifier();
+ TypeDeclaration td = findEnclosingType(sn);
+ if (td != null) {
+ String value = findValueFromField(td, fieldName);
+ if (value != null) {
+ return State.of(fieldName, value);
+ }
+ }
+ }
+
if (expr instanceof StringLiteral sl)
return State.of(sl.getLiteralValue());
@@ -75,6 +169,55 @@ public class CodebaseContext {
return State.of(raw);
}
+ private String findValueFromField(TypeDeclaration td, String fieldName) {
+ for (FieldDeclaration fd : td.getFields()) {
+ for (Object fragObj : fd.fragments()) {
+ if (fragObj instanceof VariableDeclarationFragment fragment) {
+ if (fragment.getName().getIdentifier().equals(fieldName)) {
+ // Check for @Value annotation
+ for (Object mod : fd.modifiers()) {
+ if (mod instanceof Annotation ann) {
+ String name = ann.getTypeName().getFullyQualifiedName();
+ if (name.endsWith("Value")) {
+ String placeholder = extractAnnotationValue(ann);
+ return propertyResolver.resolveValue(placeholder, properties);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private String extractAnnotationValue(Annotation ann) {
+ if (ann instanceof SingleMemberAnnotation sma) {
+ return stripQuotes(sma.getValue().toString());
+ } else if (ann instanceof NormalAnnotation na) {
+ for (Object pairObj : na.values()) {
+ MemberValuePair pair = (MemberValuePair) pairObj;
+ if ("value".equals(pair.getName().getIdentifier())) {
+ return stripQuotes(pair.getValue().toString());
+ }
+ }
+ }
+ return null;
+ }
+
+ private String stripQuotes(String s) {
+ if (s == null) return null;
+ return s.replaceAll("^\"|\"$", "");
+ }
+
+ private TypeDeclaration findEnclosingType(ASTNode node) {
+ ASTNode parent = node.getParent();
+ while (parent != null && !(parent instanceof TypeDeclaration)) {
+ parent = parent.getParent();
+ }
+ return (TypeDeclaration) parent;
+ }
+
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
String qualifier = qn.getQualifier().toString();
String name = qn.getName().getIdentifier();
@@ -97,11 +240,15 @@ public class CodebaseContext {
return name;
}
- private CompilationUnit parse(String source) {
+ private CompilationUnit parse(String source, String unitName) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
- parser.setResolveBindings(false);
+ parser.setResolveBindings(resolveBindings);
+ if (resolveBindings) {
+ parser.setUnitName(unitName);
+ parser.setEnvironment(classpath, sourcepath, null, true);
+ }
return (CompilationUnit) parser.createAST(null);
}
@@ -321,4 +468,8 @@ public class CodebaseContext {
private String getSimpleName(Type type) {
return AstUtils.extractSimpleTypeName(type);
}
+
+ public Collection getCompilationUnits() {
+ return Collections.unmodifiableCollection(allCus);
+ }
}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java
index 84e52a9..7a300bc 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/FileUtils.java
@@ -1,8 +1,11 @@
package click.kamil.springstatemachineexporter.ast.common;
import java.io.IOException;
+import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@@ -10,6 +13,14 @@ import java.util.stream.Stream;
public class FileUtils {
public static List findFilesWithExtension(Set startDirs, String extension) throws RuntimeException {
+ return findFilesWithExtension(startDirs, extension, Collections.emptySet());
+ }
+
+ public static List findFilesWithExtension(Set startDirs, String extension, Set ignorePatterns) throws RuntimeException {
+ List matchers = ignorePatterns.stream()
+ .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p))
+ .collect(Collectors.toList());
+
try (Stream paths = startDirs.stream()
.flatMap(startDir -> {
try {
@@ -20,6 +31,7 @@ public class FileUtils {
})) {
return paths
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
+ .filter(p -> matchers.stream().noneMatch(m -> m.matches(p)))
.collect(Collectors.toList());
}
}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java
index 7f6d7f6..18d7b9a 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/Dot.java
@@ -19,21 +19,20 @@ public class Dot implements StateMachineExporter {
@Override
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
- return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
- }
-
- @Override
- public String export(String name,
- List transitions,
- Set startStates,
- Set endStates,
- ExportOptions options) {
StringBuilder sb = new StringBuilder();
sb.append("digraph statemachine {\n");
sb.append(" rankdir=LR;\n");
sb.append(" node [shape=rounded, style=filled, fillcolor=white, fontname=\"Arial\"];\n");
sb.append(" edge [fontname=\"Arial\", fontsize=10];\n\n");
+ // --- Core State Machine Rendering ---
+ renderCore(result.getTransitions(), result.getStartStates(), result.getEndStates(), options, sb);
+
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ private void renderCore(List transitions, Set startStates, Set endStates, ExportOptions options, StringBuilder sb) {
// Choices
Set statesWithChoice = new LinkedHashSet<>();
Map choiceColorMap = new HashMap<>();
@@ -187,7 +186,22 @@ public class Dot implements StateMachineExporter {
}
}
}
+ }
+ @Override
+ public String export(String name,
+ List transitions,
+ Set startStates,
+ Set endStates,
+ ExportOptions options) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("digraph statemachine {\n");
+ sb.append(" rankdir=LR;\n");
+ sb.append(" node [shape=rounded, style=filled, fillcolor=white, fontname=\"Arial\"];\n");
+ sb.append(" edge [fontname=\"Arial\", fontsize=10];\n\n");
+
+ renderCore(transitions, startStates, endStates, options, sb);
+
sb.append("}\n");
return sb.toString();
}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java
index d85b2cc..34a502c 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/JsonExporter.java
@@ -19,7 +19,19 @@ public class JsonExporter implements StateMachineExporter {
@Override
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
- return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
+ try {
+ java.util.Map output = new java.util.HashMap<>();
+ output.put("name", result.getName());
+ output.put("transitions", result.getTransitions());
+ output.put("startStates", result.getStartStates());
+ output.put("endStates", result.getEndStates());
+ output.put("renderChoicesAsDiamonds", options.isRenderChoicesAsDiamonds());
+ output.put("metadata", result.getMetadata());
+
+ return objectMapper.writeValueAsString(output);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to export to JSON", e);
+ }
}
@Override
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java
index 3761387..40f104c 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/exporter/PlantUml.java
@@ -11,6 +11,7 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -35,19 +36,7 @@ public class PlantUml implements StateMachineExporter {
@Override
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
- String baseContent = export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
-
- // Allow custom styling from metadata
- String customStyle = result.getMetadata().getProperties().get("plantuml.style");
- if (customStyle != null && !customStyle.isBlank()) {
- // Insert custom style before @enduml
- int endumlIndex = baseContent.lastIndexOf("@enduml");
- if (endumlIndex >= 0) {
- return baseContent.substring(0, endumlIndex) + "\n" + customStyle + "\n" + baseContent.substring(endumlIndex);
- }
- }
-
- return baseContent;
+ return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
}
@Override
@@ -59,6 +48,18 @@ public class PlantUml implements StateMachineExporter {
StringBuilder sb = new StringBuilder();
sb.append("@startuml\n");
+ sb.append("!pragma layout smetana\n");
+ sb.append("hide empty description\n");
+ sb.append("hide stereotype\n");
+
+ // Pre-declare all unique states for layout stability
+ Set allUniqueStates = new LinkedHashSet<>();
+ transitions.forEach(t -> {
+ t.getSourceStates().forEach(s -> allUniqueStates.add(simplify(s.toString())));
+ t.getTargetStates().forEach(s -> allUniqueStates.add(simplify(s.toString())));
+ });
+ allUniqueStates.forEach(s -> sb.append("state ").append(s).append("\n"));
+ sb.append("\n");
String style = loadBaseStyle();
@@ -82,7 +83,7 @@ public class PlantUml implements StateMachineExporter {
Set statesWithChoice = new java.util.LinkedHashSet<>();
for (Transition t : transitions) {
- if (t.getType() == TransitionType.CHOICE) {
+ if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) {
for (State source : t.getSourceStates()) {
statesWithChoice.add(simplify(source.toString()));
}
@@ -137,11 +138,10 @@ public class PlantUml implements StateMachineExporter {
for (String rawTarget : targets) {
String target = simplify(rawTarget);
- String transitionSource = source;
String styleClass = getStyleClass(t, source, choiceStateClassMap);
String color = getLegacyColor(t, source, choiceStateClassMap);
- sb.append(transitionSource).append(" -[#").append(color).append(",bold]-> ").append(target).append(" <<").append(styleClass).append(">>");
+ sb.append(source).append(" -[#").append(color).append(",bold]-> ").append(target).append(" <<").append(styleClass).append(">>");
if (t.getEvent() != null && !t.getEvent().isBlank()) {
String eventClass = "e_" + normalize(t.getEvent());
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java
index 1f4538b..0498617 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java
@@ -1,7 +1,13 @@
package click.kamil.springstatemachineexporter.service;
+import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
+import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
+import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
+import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
+import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
+import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
@@ -10,7 +16,6 @@ import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
import click.kamil.springstatemachineexporter.exporter.StateMachineExporter;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
@@ -25,36 +30,62 @@ import java.util.List;
import java.util.Set;
@Slf4j
-@RequiredArgsConstructor
public class ExportService {
private final List exporters;
private final EnrichmentService enrichmentService;
+
+ public ExportService(List exporters, EnrichmentService enrichmentService) {
+ this.exporters = exporters;
+ this.enrichmentService = enrichmentService;
+ }
+
+ public ExportService(List exporters) {
+ this(exporters, new EnrichmentService(List.of(
+ new TriggerEnricher(),
+ new EntryPointEnricher(),
+ new PropertyEnricher(),
+ new CallChainEnricher()
+ )));
+ }
+
private static final List TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
public static final Set STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
public void runExporter(Path inputDir, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
+ runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList());
+ }
+
+ public void runExporter(Path inputDir, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds, List activeProfiles) throws IOException {
CodebaseContext context = new CodebaseContext();
+ context.setActiveProfiles(activeProfiles);
+ context.setSourcepath(List.of(inputDir.toString()));
+ context.setResolveBindings(true);
+
+ Path hintsFile = inputDir.resolve("hints.json");
+ if (!Files.exists(hintsFile)) {
+ hintsFile = inputDir.resolve("src/main/resources/hints.json");
+ }
+
+ if (Files.exists(hintsFile)) {
+ log.info("Loading hints from {}", hintsFile.toAbsolutePath());
+ context.loadLibraryHints(hintsFile);
+ }
+
context.scan(inputDir);
- exportAll(context, outputDir, selectedFormats, renderChoicesAsDiamonds);
+
+ CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
+ exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds);
}
public void runJsonExporter(Path jsonFile, Path outputDir, List selectedFormats) throws IOException {
JsonImportService jsonImportService = new JsonImportService();
- StateMachineModel model = jsonImportService.importJson(jsonFile);
+ AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
- AnalysisResult result = AnalysisResult.builder()
- .name(model.getName())
- .transitions(model.getTransitions())
- .startStates(model.getStartStates())
- .endStates(model.getEndStates())
- .renderChoicesAsDiamonds(model.isRenderChoicesAsDiamonds())
- .build();
-
generateOutputs(outputDir, result, selectedFormats);
}
- private void exportAll(CodebaseContext context, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
+ private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
Set processedLocations = new HashSet<>();
// 1. Find entry point classes (annotated or extending adapter)
@@ -62,7 +93,7 @@ public class ExportService {
for (TypeDeclaration td : entryPoints) {
String fqn = context.getFqn(td);
if (processedLocations.add(fqn)) {
- processEntryPoint(td, outputDir, context, selectedFormats, renderChoicesAsDiamonds);
+ processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds);
}
}
@@ -75,12 +106,12 @@ public class ExportService {
String uniqueId = parentFqn + "#" + methodName;
if (processedLocations.add(uniqueId)) {
- processBeanMethod(m, outputDir, context, selectedFormats, renderChoicesAsDiamonds);
+ processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds);
}
}
}
- private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
+ private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
String className = context.getFqn(td);
log.info("Processing state machine config: {}", className);
@@ -105,12 +136,12 @@ public class ExportService {
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
.build();
- enrichmentService.enrich(result, context);
+ enrichmentService.enrich(result, context, intelligence);
generateOutputs(outputDir, result, selectedFormats);
}
- private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
+ private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
String parentFqn = context.getFqn(parentClass);
String methodName = m.getName().getIdentifier();
@@ -131,29 +162,32 @@ public class ExportService {
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
.build();
- enrichmentService.enrich(result, context);
+ enrichmentService.enrich(result, context, intelligence);
generateOutputs(outputDir, result, selectedFormats);
}
private void generateOutputs(Path outputDir, AnalysisResult result, List selectedFormats) throws IOException {
+ if (selectedFormats == null || selectedFormats.isEmpty()) {
+ return;
+ }
- Path targetDir = outputDir.resolve(result.getName());
+ String actualName = result.getName();
+ Path targetDir = outputDir.resolve(actualName);
Files.createDirectories(targetDir);
ExportOptions options = ExportOptions.builder()
- .useLambdaGuards(true)
.renderChoicesAsDiamonds(result.isRenderChoicesAsDiamonds())
.build();
- for (StateMachineExporter output : exporters) {
- if (selectedFormats != null && !selectedFormats.isEmpty()) {
- boolean match = selectedFormats.stream().anyMatch(f -> output.getFileExtension().toLowerCase().contains(f.toLowerCase()));
- if (!match) continue;
+ for (StateMachineExporter exporter : exporters) {
+ String ext = exporter.getFileExtension().toLowerCase();
+ if (!selectedFormats.stream().anyMatch(fmt -> ext.contains(fmt.toLowerCase()))) {
+ continue;
}
- String content = output.export(result, options);
- String fileName = result.getName() + output.getFileExtension();
+ String content = exporter.export(result, options);
+ String fileName = result.getName() + exporter.getFileExtension();
try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) {
out.println(content);
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java
index 6177645..3b62c7a 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/JsonImportService.java
@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter.service;
+import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.model.StateMachineModel;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -23,4 +24,8 @@ public class JsonImportService {
}
return model;
}
+
+ public AnalysisResult importAnalysisResult(Path jsonFile) throws IOException {
+ return objectMapper.readValue(jsonFile.toFile(), AnalysisResult.class);
+ }
}
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java
index 13cba55..1f24670 100644
--- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java
+++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlE2ETest.java
@@ -28,8 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class PlantUmlE2ETest {
private final List exporters = List.of(new PlantUml());
- private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
- private final ExportService exportService = new ExportService(exporters, enrichmentService);
+ private final ExportService exportService = new ExportService(exporters);
private static Path findProjectRoot() {
Path current = Path.of(".").toAbsolutePath();
@@ -107,6 +106,31 @@ public class PlantUmlE2ETest {
root.resolve("state_machines/inheritance_extra_functions3_state_machine"),
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
"G2StateMachineConfiguration"
+ ),
+ new TestScenario(
+ "Extended Analysis Sample",
+ root.resolve("state_machines/extended_analysis_sample"),
+ Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
+ "ExtendedStateMachineConfig"
+ ),
+ new TestScenario(
+ "Extended Analysis Sample (PROD)",
+ root.resolve("state_machines/extended_analysis_sample"),
+ Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"),
+ "ExtendedStateMachineConfig",
+ List.of("prod")
+ ),
+ new TestScenario(
+ "Inheritance Sample",
+ root.resolve("state_machines/inheritance_sample"),
+ Path.of("src/test/resources/golden/InheritanceStateMachineConfig"),
+ "InheritanceStateMachineConfig"
+ ),
+ new TestScenario(
+ "Enterprise Order System",
+ root.resolve("state_machines/enterprise_order_system"),
+ Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
+ "EnterpriseStateMachineConfig"
)
);
}
@@ -115,7 +139,7 @@ public class PlantUmlE2ETest {
@MethodSource("provideTestScenarios")
void testPngGenerationMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception {
// 1. Generate PUML
- exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml"), true);
+ exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml"), true, scenario.activeProfiles());
List generatedDirs;
try (var stream = Files.list(tempDir)) {
@@ -129,34 +153,42 @@ public class PlantUmlE2ETest {
.orElseThrow(() -> new IllegalArgumentException("No output directory found for " + targetBaseName));
String actualBaseName = actualDir.getFileName().toString();
- Path pumlFile = actualDir.resolve(actualBaseName + ".puml");
-
- assertThat(pumlFile).exists();
- // 2. Render PNG using PlantUML library
- String pumlContent = Files.readString(pumlFile);
- SourceStringReader reader = new SourceStringReader(pumlContent);
- ByteArrayOutputStream os = new ByteArrayOutputStream();
- reader.outputImage(os, new FileFormatOption(FileFormat.PNG));
- byte[] actualPngBytes = os.toByteArray();
+ List pumlFiles;
+ try (var stream = Files.list(actualDir)) {
+ pumlFiles = stream.filter(p -> p.toString().endsWith(".puml")).toList();
+ }
- // 3. Compare with Golden
- Path goldenPngFile = scenario.goldenPath().resolve(targetBaseName + ".png");
-
- if (Boolean.getBoolean("updateGolden")) {
- Files.createDirectories(goldenPngFile.getParent());
- Files.write(goldenPngFile, actualPngBytes);
- System.out.println("Updated golden PNG: " + goldenPngFile);
- } else {
- assertThat(goldenPngFile)
- .as("Golden PNG file %s should exist", goldenPngFile)
- .exists();
+ for (Path pumlFile : pumlFiles) {
+ String fileName = pumlFile.getFileName().toString();
+ String baseNoExt = fileName.substring(0, fileName.lastIndexOf('.'));
+ String goldenBaseNoExt = baseNoExt.replace(actualBaseName, targetBaseName);
- byte[] expectedPngBytes = Files.readAllBytes(goldenPngFile);
-
- assertThat(actualPngBytes)
- .as("Visual regression detected for %s. PNG output does not match golden reference.", scenario.name())
- .containsExactly(expectedPngBytes);
+ // 2. Render PNG using PlantUML library
+ String pumlContent = Files.readString(pumlFile);
+ SourceStringReader reader = new SourceStringReader(pumlContent);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ reader.outputImage(os, new FileFormatOption(FileFormat.PNG));
+ byte[] actualPngBytes = os.toByteArray();
+
+ // 3. Compare with Golden
+ Path goldenPngFile = scenario.goldenPath().resolve(goldenBaseNoExt + ".png");
+
+ if (System.getProperty("updateGolden", "false").equals("true")) {
+ Files.createDirectories(scenario.goldenPath());
+ Files.write(goldenPngFile, actualPngBytes);
+ System.out.println("Updated golden PNG: " + goldenPngFile);
+ } else {
+ assertThat(goldenPngFile)
+ .as("Golden PNG missing for %s", fileName)
+ .exists();
+
+ byte[] expectedPngBytes = Files.readAllBytes(goldenPngFile);
+
+ assertThat(actualPngBytes)
+ .as("Visual regression detected for %s (%s). PNG output does not match golden reference.", scenario.name(), fileName)
+ .containsExactly(expectedPngBytes);
+ }
}
}
}
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java
index 165804d..4f189f6 100644
--- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java
+++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java
@@ -22,8 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class RegressionTest {
private final List exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
- private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
- private final ExportService exportService = new ExportService(exporters, enrichmentService);
+ private final ExportService exportService = new ExportService(exporters);
private static Path findProjectRoot() {
Path current = Path.of(".").toAbsolutePath();
@@ -101,6 +100,31 @@ public class RegressionTest {
root.resolve("state_machines/inheritance_extra_functions3_state_machine"),
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
"G2StateMachineConfiguration"
+ ),
+ new TestScenario(
+ "Extended Analysis Sample",
+ root.resolve("state_machines/extended_analysis_sample"),
+ Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
+ "ExtendedStateMachineConfig"
+ ),
+ new TestScenario(
+ "Extended Analysis Sample (PROD)",
+ root.resolve("state_machines/extended_analysis_sample"),
+ Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"),
+ "ExtendedStateMachineConfig",
+ List.of("prod")
+ ),
+ new TestScenario(
+ "Inheritance Sample",
+ root.resolve("state_machines/inheritance_sample"),
+ Path.of("src/test/resources/golden/InheritanceStateMachineConfig"),
+ "InheritanceStateMachineConfig"
+ ),
+ new TestScenario(
+ "Enterprise Order System",
+ root.resolve("state_machines/enterprise_order_system"),
+ Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
+ "EnterpriseStateMachineConfig"
)
);
}
@@ -108,7 +132,12 @@ public class RegressionTest {
@ParameterizedTest(name = "{0}")
@MethodSource("provideTestScenarios")
void testOutputMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception {
- exportService.runExporter(scenario.inputPath(), tempDir, null, true);
+ System.out.println("Running test for " + scenario.name());
+ if (exportService == null) System.out.println("exportService is NULL");
+ if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
+ if (tempDir == null) System.out.println("tempDir is NULL");
+
+ exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles());
// Find the generated directory (it might be named with FQN)
List generatedDirs;
@@ -121,28 +150,33 @@ public class RegressionTest {
.filter(d -> d.getFileName().toString().endsWith(targetBaseName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No output directory found for " + targetBaseName + " in " + generatedDirs));
+String actualBaseName = actualDir.getFileName().toString();
- String actualBaseName = actualDir.getFileName().toString();
+List generatedFiles;
+try (var stream = Files.list(actualDir)) {
+ generatedFiles = stream.filter(Files::isRegularFile).toList();
+}
- for (StateMachineExporter exporter : exporters) {
- String fileName = actualBaseName + exporter.getFileExtension();
- String goldenFileName = targetBaseName + exporter.getFileExtension();
+for (Path actualFile : generatedFiles) {
+ String fileName = actualFile.getFileName().toString();
+ // Map actual filename to golden filename (handle FQN difference)
+ String goldenFileName = fileName.replace(actualBaseName, targetBaseName);
+ Path goldenFile = scenario.goldenPath().resolve(goldenFileName);
- Path actualFile = actualDir.resolve(fileName);
- Path goldenFile = scenario.goldenPath().resolve(goldenFileName);
+ if (System.getProperty("updateGolden", "false").equals("true")) {
+ Files.createDirectories(scenario.goldenPath());
+ Files.copy(actualFile, goldenFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ System.out.println("Updated golden file: " + goldenFile);
+ } else {
+ assertThat(goldenFile)
+ .as("Golden file missing for %s", fileName)
+ .exists();
- if (Boolean.getBoolean("updateGolden")) {
- Files.createDirectories(goldenFile.getParent());
- Files.copy(actualFile, goldenFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
- } else {
- assertThat(actualFile)
- .as("File %s should exist in output", fileName)
- .exists();
-
- assertThat(actualFile)
- .as("Content mismatch for %s", fileName)
- .content().isEqualTo(Files.readString(goldenFile));
- }
- }
+ assertThat(actualFile)
+ .as("Content mismatch for %s", fileName)
+ .content().isEqualTo(Files.readString(goldenFile));
}
}
+}
+}
+
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/AnalysisFoundationTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/AnalysisFoundationTest.java
new file mode 100644
index 0000000..750d7ca
--- /dev/null
+++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/AnalysisFoundationTest.java
@@ -0,0 +1,83 @@
+package click.kamil.springstatemachineexporter.analysis;
+
+import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
+import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.Expression;
+import org.eclipse.jdt.core.dom.FieldDeclaration;
+import org.eclipse.jdt.core.dom.TypeDeclaration;
+import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AnalysisFoundationTest {
+
+ @Test
+ void testPropertyResolver(@TempDir Path tempDir) throws IOException {
+ Files.writeString(tempDir.resolve("application.properties"), "app.event=SUBMIT\napp.path=/api/v1");
+
+ PropertyResolver resolver = new PropertyResolver();
+ Map props = resolver.resolveProperties(tempDir);
+
+ assertThat(props).containsEntry("app.event", "SUBMIT");
+ assertThat(props).containsEntry("app.path", "/api/v1");
+ }
+
+ @Test
+ void testConstantResolverManualFallback(@TempDir Path tempDir) throws IOException {
+ // Create a class with a constant
+ Path constDir = tempDir.resolve("com/example");
+ Files.createDirectories(constDir);
+ Files.writeString(constDir.resolve("Events.java"),
+ "package com.example;\n" +
+ "public class Events {\n" +
+ " public static final String SUBMIT = \"SUBMIT_EVENT\";\n" +
+ "}");
+
+ // Create a class that uses the constant
+ Files.writeString(constDir.resolve("Usage.java"),
+ "package com.example;\n" +
+ "public class Usage {\n" +
+ " String e = Events.SUBMIT;\n" +
+ "}");
+
+ CodebaseContext context = new CodebaseContext();
+ context.scan(tempDir);
+
+ TypeDeclaration usageTd = context.getTypeDeclaration("com.example.Usage");
+ FieldDeclaration field = usageTd.getFields()[0];
+ VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
+ Expression initializer = fragment.getInitializer();
+
+ ConstantResolver resolver = new ConstantResolver();
+ String value = resolver.resolve(initializer, context);
+
+ assertThat(value).isEqualTo("SUBMIT_EVENT");
+ }
+
+ @Test
+ void testPropertyResolverWithProfiles(@TempDir Path tempDir) throws IOException {
+ Files.writeString(tempDir.resolve("application.properties"), "app.event=BASE");
+ Files.writeString(tempDir.resolve("application-prod.properties"), "app.event=PROD");
+
+ PropertyResolver resolver = new PropertyResolver();
+
+ // 1. Base only
+ Map baseProps = resolver.resolveProperties(tempDir);
+ assertThat(baseProps).containsEntry("app.event", "BASE");
+
+ // 2. With prod profile
+ resolver.setActiveProfiles(List.of("prod"));
+ Map prodProps = resolver.resolveProperties(tempDir);
+ assertThat(prodProps).containsEntry("app.event", "PROD");
+ }
+}
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/exporter/PlantUmlCustomStyleTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/exporter/PlantUmlCustomStyleTest.java
deleted file mode 100644
index f4d0422..0000000
--- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/exporter/PlantUmlCustomStyleTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package click.kamil.springstatemachineexporter.exporter;
-
-import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
-import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
-import click.kamil.springstatemachineexporter.model.State;
-import click.kamil.springstatemachineexporter.model.Transition;
-import click.kamil.springstatemachineexporter.model.TransitionType;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class PlantUmlCustomStyleTest {
-
- @Test
- void shouldApplyCustomStyleFromMetadata() {
- PlantUml exporter = new PlantUml();
- Transition transition = new Transition();
- transition.setType(TransitionType.EXTERNAL);
- transition.setSourceStates(List.of(State.of("S1")));
- transition.setTargetStates(List.of(State.of("S2")));
- transition.setEvent("E1");
-
- AnalysisResult result = AnalysisResult.builder()
- .name("TestSM")
- .transitions(List.of(transition))
- .startStates(Set.of("S1"))
- .endStates(Set.of("S2"))
- .metadata(CodebaseMetadata.builder()
- .properties(Map.of("plantuml.style", "skinparam stateBackgroundColor Red"))
- .build())
- .build();
-
- String puml = exporter.export(result, ExportOptions.builder().build());
-
- assertThat(puml)
- .contains("skinparam stateBackgroundColor Red")
- .contains("@enduml");
-
- // Ensure it's inserted before @enduml
- int styleIndex = puml.indexOf("skinparam stateBackgroundColor Red");
- int endumlIndex = puml.indexOf("@enduml");
- assertThat(styleIndex).isLessThan(endumlIndex);
- }
-}
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java
index ae903e9..53d09b3 100644
--- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java
+++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/service/JsonImportServiceTest.java
@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter.service;
+import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.model.StateMachineModel;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -39,4 +40,33 @@ class JsonImportServiceTest {
assertThat(model.getStartStates()).containsExactly("S1");
assertThat(model.getEndStates()).containsExactly("S2");
}
+
+ @Test
+ void shouldImportRichAnalysisResultFromJson(@TempDir Path tempDir) throws IOException {
+ Path jsonFile = tempDir.resolve("rich.json");
+ String json = """
+ {
+ "name" : "RichSM",
+ "transitions" : [],
+ "startStates" : [],
+ "endStates" : [],
+ "metadata" : {
+ "entryPoints" : [ {
+ "type" : "REST",
+ "name" : "POST /rich",
+ "className" : "RichController",
+ "methodName" : "submit"
+ } ]
+ }
+ }
+ """;
+ Files.writeString(jsonFile, json);
+
+ JsonImportService importService = new JsonImportService();
+ AnalysisResult result = importService.importAnalysisResult(jsonFile);
+
+ assertThat(result.getName()).isEqualTo("RichSM");
+ assertThat(result.getMetadata().getEntryPoints()).hasSize(1);
+ assertThat(result.getMetadata().getEntryPoints().get(0).getName()).isEqualTo("POST /rich");
+ }
}
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java
index 4970f46..dcc65a3 100644
--- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java
+++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/utils/TestScenario.java
@@ -1,10 +1,17 @@
package click.kamil.springstatemachineexporter.utils;
import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
public record TestScenario(
String name,
Path inputPath,
Path goldenPath,
- String baseName
-) {}
+ String baseName,
+ List activeProfiles
+) {
+ public TestScenario(String name, Path inputPath, Path goldenPath, String baseName) {
+ this(name, inputPath, goldenPath, baseName, Collections.emptyList());
+ }
+}
diff --git a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json
index e963af9..d510c35 100644
--- a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json
+++ b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.json
@@ -1,5 +1,15 @@
{
+ "metadata" : {
+ "triggers" : [ ],
+ "entryPoints" : [ ],
+ "callChains" : [ ],
+ "properties" : {
+ "spring.application.name" : "statemachinedemo"
+ }
+ },
"name" : "click.kamil.examples.statemachine.complex.ComplexStateMachineConfig",
+ "renderChoicesAsDiamonds" : true,
+ "startStates" : [ "States.STATE1" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
@@ -572,7 +582,5 @@
"actions" : [ ],
"order" : 1
} ],
- "startStates" : [ "States.STATE1" ],
- "endStates" : [ ],
- "renderChoicesAsDiamonds" : true
+ "endStates" : [ ]
}
diff --git a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png
index 1283c7f..fe07a47 100644
Binary files a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png and b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.png differ
diff --git a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml
index 84428d8..1537e6c 100644
--- a/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml
+++ b/state_machine_exporter/src/test/resources/golden/ComplexStateMachineConfig/ComplexStateMachineConfig.puml
@@ -1,4 +1,28 @@
@startuml
+!pragma layout smetana
+hide empty description
+hide stereotype
+state STATE1
+state STATE2
+state STATE3
+state STATE4
+state STATE5
+state STATE6
+state STATE7
+state STATE8
+state STATE9
+state STATE10
+state STATE11
+state STATE12
+state STATE13
+state STATE14
+state STATE15
+state STATE16
+state STATE17
+state STATE18
+state STATE19
+state STATE20
+
+
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+
+[*] --> NEW
+
+state CHECK_AVAILABILITY <>
+
+NEW -[#1E90FF,bold]-> CHECK_AVAILABILITY <> <> : PLACE_ORDER
+CHECK_AVAILABILITY -[#FF6347,bold]-> PENDING_PAYMENT <> : [λ] (order=0)
+CHECK_AVAILABILITY -[#FF6347,bold]-> CANCELLED <> : (order=1)
+PENDING_PAYMENT -[#1E90FF,bold]-> PAID <> <> : PAY_ORDER
+PAID -[#1E90FF,bold]-> SHIPPED <> <> : SHIP_ORDER
+SHIPPED -[#1E90FF,bold]-> DELIVERED <> <> : FINALIZE
+PAID -[#1E90FF,bold]-> CANCELLED <> <> : CANCEL_ORDER
+DELIVERED -[#1E90FF,bold]-> RETURNED <> <> : RETURN_ORDER
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+
+CANCELLED --> [*]
+DELIVERED --> [*]
+RETURNED --> [*]
+@enduml
+
diff --git a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.scxml.xml
new file mode 100644
index 0000000..367c9bf
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.scxml.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.dot b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.dot
new file mode 100644
index 0000000..a13f5ef
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.dot
@@ -0,0 +1,17 @@
+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 -> INIT_STATE;
+ CANCELLED [fillcolor=lightgray];
+ COMPLETED [fillcolor=lightgray];
+ START -> PROCESSING [label="SUBMIT_EVENT", style="solid", color="black"];
+ PROCESSING -> COMPLETED [label="FINISH", style="solid", color="black"];
+ PROCESSING -> CANCELLED [label="CANCEL_EVENT", style="solid", color="black"];
+ START -> PROCESSING [label="REACTIVE_EVENT", style="solid", color="black"];
+ START -> START [label="AUDIT_EVENT", style="solid", color="black"];
+ START -> PROCESSING [label="EXTERNAL_TRIGGER", style="solid", color="black"];
+}
+
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json
new file mode 100644
index 0000000..0c6b2c3
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json
@@ -0,0 +1,340 @@
+{
+ "metadata" : {
+ "triggers" : [ {
+ "event" : "FINISH",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 34
+ }, {
+ "event" : "AUDIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 16
+ }, {
+ "event" : "EXTERNAL_TRIGGER",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 19
+ }, {
+ "event" : "SUBMIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 20
+ }, {
+ "event" : "CANCEL_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processCancel",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 25
+ }, {
+ "event" : "[LIFECYCLE:RESTORE]",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "resumeOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 29
+ }, {
+ "event" : "REACTIVE_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
+ "methodName" : "processReactive",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 18
+ } ],
+ "entryPoints" : [ {
+ "type" : "REST",
+ "name" : "POST /api/orders/submit",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "submitOrder",
+ "metadata" : {
+ "path" : "/api/orders/submit",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/cancel",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "cancelOrder",
+ "metadata" : {
+ "path" : "/api/orders/cancel",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/finish",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "metadata" : {
+ "path" : "/api/orders/finish",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/resume",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "resumeOrder",
+ "metadata" : {
+ "path" : "/api/orders/resume",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/reactive",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "reactiveOrder",
+ "metadata" : {
+ "path" : "/api/orders/reactive",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "CUSTOM",
+ "name" : "INTERCEPTOR: AuditInterceptor.preHandle",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "metadata" : {
+ "interceptorType" : "Spring MVC Interceptor"
+ }
+ } ],
+ "callChains" : [ {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/submit",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "submitOrder",
+ "metadata" : {
+ "path" : "/api/orders/submit",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
+ "triggerPoint" : {
+ "event" : "EXTERNAL_TRIGGER",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 19
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/submit",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "submitOrder",
+ "metadata" : {
+ "path" : "/api/orders/submit",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
+ "triggerPoint" : {
+ "event" : "SUBMIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 20
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/cancel",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "cancelOrder",
+ "metadata" : {
+ "path" : "/api/orders/cancel",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.cancelOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processCancel" ],
+ "triggerPoint" : {
+ "event" : "CANCEL_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processCancel",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 25
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/finish",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "metadata" : {
+ "path" : "/api/orders/finish",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.finishOrder" ],
+ "triggerPoint" : {
+ "event" : "FINISH",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 34
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/resume",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "resumeOrder",
+ "metadata" : {
+ "path" : "/api/orders/resume",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.resumeOrder", "click.kamil.examples.statemachine.extended.service.OrderService.resumeOrder" ],
+ "triggerPoint" : {
+ "event" : "[LIFECYCLE:RESTORE]",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "resumeOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 29
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/reactive",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "reactiveOrder",
+ "metadata" : {
+ "path" : "/api/orders/reactive",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ],
+ "triggerPoint" : {
+ "event" : "REACTIVE_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
+ "methodName" : "processReactive",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 18
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "CUSTOM",
+ "name" : "INTERCEPTOR: AuditInterceptor.preHandle",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "metadata" : {
+ "interceptorType" : "Spring MVC Interceptor"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.AuditInterceptor.preHandle" ],
+ "triggerPoint" : {
+ "event" : "AUDIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 16
+ }
+ } ],
+ "properties" : {
+ "app.states.initial" : "INIT_STATE"
+ }
+ },
+ "name" : "click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig",
+ "renderChoicesAsDiamonds" : true,
+ "startStates" : [ "INIT_STATE" ],
+ "transitions" : [ {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "event" : "SUBMIT_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"COMPLETED\"",
+ "fullIdentifier" : "COMPLETED"
+ } ],
+ "event" : "FINISH",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"CANCELLED\"",
+ "fullIdentifier" : "CANCELLED"
+ } ],
+ "event" : "CANCEL_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "event" : "REACTIVE_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "event" : "AUDIT_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "event" : "EXTERNAL_TRIGGER",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ } ],
+ "endStates" : [ "CANCELLED", "COMPLETED" ]
+}
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.png b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.png
new file mode 100644
index 0000000..0c6feee
Binary files /dev/null and b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.png differ
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.puml
new file mode 100644
index 0000000..ee671a1
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.puml
@@ -0,0 +1,70 @@
+@startuml
+!pragma layout smetana
+hide empty description
+hide stereotype
+state START
+state PROCESSING
+state COMPLETED
+state CANCELLED
+
+
+
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+
+[*] --> INIT_STATE
+
+
+START -[#1E90FF,bold]-> PROCESSING <> <> : SUBMIT_EVENT
+PROCESSING -[#1E90FF,bold]-> COMPLETED <> <> : FINISH
+PROCESSING -[#1E90FF,bold]-> CANCELLED <> <> : CANCEL_EVENT
+START -[#1E90FF,bold]-> PROCESSING <> <> : REACTIVE_EVENT
+START -[#1E90FF,bold]-> START <> <> : AUDIT_EVENT
+START -[#1E90FF,bold]-> PROCESSING <> <> : EXTERNAL_TRIGGER
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+
+CANCELLED --> [*]
+COMPLETED --> [*]
+@enduml
+
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.scxml.xml
new file mode 100644
index 0000000..9094d51
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.scxml.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.dot b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.dot
new file mode 100644
index 0000000..135e343
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.dot
@@ -0,0 +1,17 @@
+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 -> PROD_INITIAL;
+ CANCELLED [fillcolor=lightgray];
+ COMPLETED [fillcolor=lightgray];
+ START -> PROCESSING [label="SUBMIT_EVENT", style="solid", color="black"];
+ PROCESSING -> COMPLETED [label="FINISH", style="solid", color="black"];
+ PROCESSING -> CANCELLED [label="CANCEL_EVENT", style="solid", color="black"];
+ START -> PROCESSING [label="REACTIVE_EVENT", style="solid", color="black"];
+ START -> START [label="AUDIT_EVENT", style="solid", color="black"];
+ START -> PROCESSING [label="EXTERNAL_TRIGGER", style="solid", color="black"];
+}
+
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json
new file mode 100644
index 0000000..35c6582
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json
@@ -0,0 +1,340 @@
+{
+ "metadata" : {
+ "triggers" : [ {
+ "event" : "FINISH",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 34
+ }, {
+ "event" : "AUDIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 16
+ }, {
+ "event" : "EXTERNAL_TRIGGER",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 19
+ }, {
+ "event" : "SUBMIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 20
+ }, {
+ "event" : "CANCEL_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processCancel",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 25
+ }, {
+ "event" : "[LIFECYCLE:RESTORE]",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "resumeOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 29
+ }, {
+ "event" : "REACTIVE_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
+ "methodName" : "processReactive",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 18
+ } ],
+ "entryPoints" : [ {
+ "type" : "REST",
+ "name" : "POST /api/orders/submit",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "submitOrder",
+ "metadata" : {
+ "path" : "/api/orders/submit",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/cancel",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "cancelOrder",
+ "metadata" : {
+ "path" : "/api/orders/cancel",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/finish",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "metadata" : {
+ "path" : "/api/orders/finish",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/resume",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "resumeOrder",
+ "metadata" : {
+ "path" : "/api/orders/resume",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "REST",
+ "name" : "POST /api/orders/reactive",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "reactiveOrder",
+ "metadata" : {
+ "path" : "/api/orders/reactive",
+ "verb" : "POST"
+ }
+ }, {
+ "type" : "CUSTOM",
+ "name" : "INTERCEPTOR: AuditInterceptor.preHandle",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "metadata" : {
+ "interceptorType" : "Spring MVC Interceptor"
+ }
+ } ],
+ "callChains" : [ {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/submit",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "submitOrder",
+ "metadata" : {
+ "path" : "/api/orders/submit",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
+ "triggerPoint" : {
+ "event" : "EXTERNAL_TRIGGER",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 19
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/submit",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "submitOrder",
+ "metadata" : {
+ "path" : "/api/orders/submit",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
+ "triggerPoint" : {
+ "event" : "SUBMIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processSubmit",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 20
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/cancel",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "cancelOrder",
+ "metadata" : {
+ "path" : "/api/orders/cancel",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.cancelOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processCancel" ],
+ "triggerPoint" : {
+ "event" : "CANCEL_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "processCancel",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 25
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/finish",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "metadata" : {
+ "path" : "/api/orders/finish",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.finishOrder" ],
+ "triggerPoint" : {
+ "event" : "FINISH",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "finishOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 34
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/resume",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "resumeOrder",
+ "metadata" : {
+ "path" : "/api/orders/resume",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.resumeOrder", "click.kamil.examples.statemachine.extended.service.OrderService.resumeOrder" ],
+ "triggerPoint" : {
+ "event" : "[LIFECYCLE:RESTORE]",
+ "className" : "click.kamil.examples.statemachine.extended.service.OrderService",
+ "methodName" : "resumeOrder",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 29
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "REST",
+ "name" : "POST /api/orders/reactive",
+ "className" : "click.kamil.examples.statemachine.extended.web.OrderController",
+ "methodName" : "reactiveOrder",
+ "metadata" : {
+ "path" : "/api/orders/reactive",
+ "verb" : "POST"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ],
+ "triggerPoint" : {
+ "event" : "REACTIVE_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
+ "methodName" : "processReactive",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 18
+ }
+ }, {
+ "entryPoint" : {
+ "type" : "CUSTOM",
+ "name" : "INTERCEPTOR: AuditInterceptor.preHandle",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "metadata" : {
+ "interceptorType" : "Spring MVC Interceptor"
+ }
+ },
+ "methodChain" : [ "click.kamil.examples.statemachine.extended.web.AuditInterceptor.preHandle" ],
+ "triggerPoint" : {
+ "event" : "AUDIT_EVENT",
+ "className" : "click.kamil.examples.statemachine.extended.web.AuditInterceptor",
+ "methodName" : "preHandle",
+ "sourceModule" : null,
+ "stateMachineId" : null,
+ "lineNumber" : 16
+ }
+ } ],
+ "properties" : {
+ "app.states.initial" : "PROD_INITIAL"
+ }
+ },
+ "name" : "click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig",
+ "renderChoicesAsDiamonds" : true,
+ "startStates" : [ "PROD_INITIAL" ],
+ "transitions" : [ {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "event" : "SUBMIT_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"COMPLETED\"",
+ "fullIdentifier" : "COMPLETED"
+ } ],
+ "event" : "FINISH",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"CANCELLED\"",
+ "fullIdentifier" : "CANCELLED"
+ } ],
+ "event" : "CANCEL_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "event" : "REACTIVE_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "event" : "AUDIT_EVENT",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ }, {
+ "type" : "EXTERNAL",
+ "sourceStates" : [ {
+ "rawName" : "\"START\"",
+ "fullIdentifier" : "START"
+ } ],
+ "targetStates" : [ {
+ "rawName" : "\"PROCESSING\"",
+ "fullIdentifier" : "PROCESSING"
+ } ],
+ "event" : "EXTERNAL_TRIGGER",
+ "guard" : null,
+ "actions" : [ ],
+ "order" : null
+ } ],
+ "endStates" : [ "CANCELLED", "COMPLETED" ]
+}
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.png b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.png
new file mode 100644
index 0000000..8727b0c
Binary files /dev/null and b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.png differ
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.puml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.puml
new file mode 100644
index 0000000..79e2ae3
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.puml
@@ -0,0 +1,70 @@
+@startuml
+!pragma layout smetana
+hide empty description
+hide stereotype
+state START
+state PROCESSING
+state COMPLETED
+state CANCELLED
+
+
+
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+
+[*] --> PROD_INITIAL
+
+
+START -[#1E90FF,bold]-> PROCESSING <> <> : SUBMIT_EVENT
+PROCESSING -[#1E90FF,bold]-> COMPLETED <> <> : FINISH
+PROCESSING -[#1E90FF,bold]-> CANCELLED <> <> : CANCEL_EVENT
+START -[#1E90FF,bold]-> PROCESSING <> <> : REACTIVE_EVENT
+START -[#1E90FF,bold]-> START <> <> : AUDIT_EVENT
+START -[#1E90FF,bold]-> PROCESSING <> <> : EXTERNAL_TRIGGER
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+
+CANCELLED --> [*]
+COMPLETED --> [*]
+@enduml
+
diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.scxml.xml b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.scxml.xml
new file mode 100644
index 0000000..a99e0dd
--- /dev/null
+++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.scxml.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json
index 55373b1..047c356 100644
--- a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json
+++ b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.json
@@ -1,5 +1,15 @@
{
+ "metadata" : {
+ "triggers" : [ ],
+ "entryPoints" : [ ],
+ "callChains" : [ ],
+ "properties" : {
+ "spring.application.name" : "statemachinedemo"
+ }
+ },
"name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.F1StateMachineConfiguration",
+ "renderChoicesAsDiamonds" : true,
+ "startStates" : [ "States.STATE1" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
@@ -774,7 +784,5 @@
"actions" : [ ],
"order" : null
} ],
- "startStates" : [ "States.STATE1" ],
- "endStates" : [ "States.STATEZ" ],
- "renderChoicesAsDiamonds" : true
+ "endStates" : [ "States.STATEZ" ]
}
diff --git a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png
index 05ce3f4..ab9e524 100644
Binary files a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png and b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.png differ
diff --git a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml
index 81e0d2d..48c76dd 100644
--- a/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml
+++ b/state_machine_exporter/src/test/resources/golden/F1StateMachineConfiguration/F1StateMachineConfiguration.puml
@@ -1,4 +1,35 @@
@startuml
+!pragma layout smetana
+hide empty description
+hide stereotype
+state STATE1
+state STATE2
+state STATE3
+state STATE4
+state STATE5
+state STATE6
+state STATE7
+state STATE8
+state STATE9
+state STATE10
+state STATE11
+state STATE12
+state STATE13
+state STATE14
+state STATE15
+state STATE16
+state CANCEL
+state STATEY
+state STATEX
+state STATEZ
+state STATE17
+state STATE18
+state STATE19
+state STATE20
+state STATE_EXTRA_1
+state STATE_EXTRA_3
+state STATE_EXTRA_2
+
+
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <> stereotype
+hide <