extended analysis
@@ -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.
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="SUBMITTED">
|
||||
<state id="PAID1">
|
||||
<transition target="PAID1" event="OrderEvents.ABCD"/>
|
||||
</state>
|
||||
<state id="SUBMITTED">
|
||||
<transition target="PAID" event="OrderEvents.PAY"/>
|
||||
<transition target="CANCELED" event="OrderEvents.CANCEL" cond="λ"/>
|
||||
<transition target="SUBMITTED" event="OrderEvents.ABCD"/>
|
||||
<transition target="PAID2" cond="λ">
|
||||
<!-- order=0 -->
|
||||
</transition>
|
||||
<transition target="PAID3">
|
||||
<!-- order=1 -->
|
||||
</transition>
|
||||
</state>
|
||||
<state id="PAID">
|
||||
<transition target="FULFILLED" event="OrderEvents.FULFILL"/>
|
||||
<transition target="CANCELED" event="OrderEvents.CANCEL"/>
|
||||
<transition target="PAID1" cond="λ">
|
||||
<!-- order=0 -->
|
||||
</transition>
|
||||
<transition target="PAID2" cond="guard1">
|
||||
<!-- order=1 -->
|
||||
</transition>
|
||||
<transition target="HAPPEN" cond="λ">
|
||||
<!-- order=2 -->
|
||||
</transition>
|
||||
<transition target="PAID3">
|
||||
<!-- order=3 -->
|
||||
</transition>
|
||||
</state>
|
||||
<state id="CANCELED">
|
||||
</state>
|
||||
<state id="FULFILLED">
|
||||
</state>
|
||||
<state id="PAID3">
|
||||
<transition target="CANCELED"/>
|
||||
</state>
|
||||
<state id="HAPPEN">
|
||||
</state>
|
||||
<state id="PAID2">
|
||||
<transition target="CANCELED"/>
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<CallChain> chains = intelligence.findCallChains(
|
||||
result.getMetadata().getEntryPoints(),
|
||||
result.getMetadata().getTriggers()
|
||||
);
|
||||
|
||||
result.addMetadata(CodebaseMetadata.builder()
|
||||
.callChains(chains)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -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<EntryPoint> entryPoints = intelligence.findEntryPoints();
|
||||
|
||||
result.addMetadata(CodebaseMetadata.builder()
|
||||
.entryPoints(entryPoints)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> properties = intelligence.resolveProperties();
|
||||
|
||||
result.addMetadata(CodebaseMetadata.builder()
|
||||
.properties(properties)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -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<TriggerPoint> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<Transition> transitions;
|
||||
@@ -23,18 +29,22 @@ public class AnalysisResult {
|
||||
public void addMetadata(CodebaseMetadata newMetadata) {
|
||||
if (newMetadata == null) return;
|
||||
|
||||
List<CodebaseMetadata.TriggerPoint> combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers());
|
||||
combinedTriggers.addAll(newMetadata.getTriggers());
|
||||
List<TriggerPoint> combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers());
|
||||
if (newMetadata.getTriggers() != null) combinedTriggers.addAll(newMetadata.getTriggers());
|
||||
|
||||
List<CodebaseMetadata.EntryPoint> combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints());
|
||||
combinedEntryPoints.addAll(newMetadata.getEntryPoints());
|
||||
List<EntryPoint> combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints());
|
||||
if (newMetadata.getEntryPoints() != null) combinedEntryPoints.addAll(newMetadata.getEntryPoints());
|
||||
|
||||
List<CallChain> combinedCallChains = new java.util.ArrayList<>(this.metadata.getCallChains());
|
||||
if (newMetadata.getCallChains() != null) combinedCallChains.addAll(newMetadata.getCallChains());
|
||||
|
||||
java.util.Map<String, String> 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();
|
||||
}
|
||||
|
||||
@@ -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<String> methodChain; // e.g., ["Controller.submit", "Service.process", "Service.send"]
|
||||
private final TriggerPoint triggerPoint;
|
||||
}
|
||||
@@ -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<TriggerPoint> triggers;
|
||||
private final List<EntryPoint> entryPoints;
|
||||
private final Map<String, String> properties;
|
||||
@Builder.Default
|
||||
private final List<TriggerPoint> triggers = Collections.emptyList();
|
||||
@Builder.Default
|
||||
private final List<EntryPoint> entryPoints = Collections.emptyList();
|
||||
@Builder.Default
|
||||
private final List<CallChain> callChains = Collections.emptyList();
|
||||
@Builder.Default
|
||||
private final Map<String, String> 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<String, String> metadata) {}
|
||||
public record EntryPoint(String className, String methodName, String type, Map<String, String> metadata) {}
|
||||
}
|
||||
|
||||
@@ -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<String, String> metadata; // e.g., path, topic, exchange
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> activeProfiles = new ArrayList<>();
|
||||
|
||||
public void setActiveProfiles(List<String> profiles) {
|
||||
this.activeProfiles.clear();
|
||||
if (profiles != null) {
|
||||
this.activeProfiles.addAll(profiles);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> resolveProperties(Path rootDir) {
|
||||
Map<String, String> 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<String, String> target) {
|
||||
try (Stream<Path> 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<String, String> loadProperties(Path path) {
|
||||
Map<String, String> 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<String, String> 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<String, String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
Map<String, List<String>> callGraph = buildCallGraph();
|
||||
List<CallChain> chains = new ArrayList<>();
|
||||
|
||||
for (EntryPoint ep : entryPoints) {
|
||||
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
||||
for (TriggerPoint tp : triggers) {
|
||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
||||
if (path != null) {
|
||||
chains.add(CallChain.builder()
|
||||
.entryPoint(ep)
|
||||
.triggerPoint(tp)
|
||||
.methodChain(path)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
return chains;
|
||||
}
|
||||
|
||||
private Map<String, List<String>> buildCallGraph() {
|
||||
Map<String, List<String>> 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<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(calledMethod);
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||
String baseCalled = resolveCalledMethod(node);
|
||||
if (baseCalled == null) return Collections.emptyList();
|
||||
|
||||
List<String> 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<String> impls = context.getImplementations(className);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
}
|
||||
}
|
||||
|
||||
return allResolved;
|
||||
}
|
||||
|
||||
private String resolveCalledMethod(MethodInvocation node) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
|
||||
if (receiver == null) {
|
||||
// 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<String> findPath(String start, String target, Map<String, List<String>> graph, Set<String> visited) {
|
||||
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||
if (!visited.add(start)) return null;
|
||||
|
||||
List<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<TriggerPoint> findTriggerPoints();
|
||||
|
||||
/**
|
||||
* Discovers all entry points (REST, Kafka, etc.).
|
||||
*/
|
||||
List<EntryPoint> findEntryPoints();
|
||||
|
||||
/**
|
||||
* Links Entry Points to Trigger Points.
|
||||
*/
|
||||
List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
|
||||
|
||||
/**
|
||||
* Resolves configuration properties.
|
||||
*/
|
||||
Map<String, String> resolveProperties();
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import java.util.List;
|
||||
public class EnrichmentService {
|
||||
private final List<AnalysisEnricher> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LibraryHint> hints;
|
||||
|
||||
public List<TriggerPoint> detect(CompilationUnit cu) {
|
||||
List<TriggerPoint> 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<TriggerPoint> 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<TriggerPoint> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<EntryPoint> detect(CompilationUnit cu) {
|
||||
List<EntryPoint> 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<EntryPoint> entryPoints) {
|
||||
String fqn = context.getFqn(node);
|
||||
for (MethodDeclaration method : node.getMethods()) {
|
||||
String methodName = method.getName().getIdentifier();
|
||||
if (isInterceptorMethod(methodName)) {
|
||||
Map<String, String> 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";
|
||||
}
|
||||
}
|
||||
@@ -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<TriggerPoint> findTriggerPoints() {
|
||||
List<TriggerPoint> 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<EntryPoint> findEntryPoints() {
|
||||
List<EntryPoint> 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<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> 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<String, String> resolveProperties() {
|
||||
return context.getProperties();
|
||||
}
|
||||
}
|
||||
@@ -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<TriggerPoint> detect(CompilationUnit cu) {
|
||||
List<TriggerPoint> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<EntryPoint> detect(CompilationUnit cu) {
|
||||
List<EntryPoint> 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<EntryPoint> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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);
|
||||
|
||||
@@ -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<String, Path> classPaths = new HashMap<>();
|
||||
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
||||
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||
private final PropertyResolver propertyResolver = new PropertyResolver();
|
||||
private Map<String, String> properties = new HashMap<>();
|
||||
private List<LibraryHint> 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<List<LibraryHint>>() {});
|
||||
System.out.println("Loaded " + libraryHints.size() + " library hints");
|
||||
}
|
||||
}
|
||||
|
||||
public List<LibraryHint> getLibraryHints() {
|
||||
return Collections.unmodifiableList(libraryHints);
|
||||
}
|
||||
|
||||
private final Set<String> 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<String, String> getProperties() {
|
||||
return Collections.unmodifiableMap(properties);
|
||||
}
|
||||
|
||||
public void setClasspath(List<String> classpath) {
|
||||
this.classpath = classpath.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public void setSourcepath(List<String> sourcepath) {
|
||||
this.sourcepath = sourcepath.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public void setResolveBindings(boolean resolveBindings) {
|
||||
this.resolveBindings = resolveBindings;
|
||||
}
|
||||
|
||||
public void setActiveProfiles(List<String> profiles) {
|
||||
this.propertyResolver.setActiveProfiles(profiles);
|
||||
}
|
||||
|
||||
public void scan(Path rootDir) throws IOException {
|
||||
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java");
|
||||
scan(rootDir, Collections.emptySet());
|
||||
}
|
||||
|
||||
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||
|
||||
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
||||
|
||||
public void scan(Path rootDir, Set<String> customIgnorePatterns) throws IOException {
|
||||
this.properties = propertyResolver.resolveProperties(rootDir);
|
||||
|
||||
Set<String> activeIgnore = new HashSet<>(ignorePatterns);
|
||||
activeIgnore.addAll(customIgnorePatterns);
|
||||
|
||||
List<Path> 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<String> getImplementations(String interfaceName) {
|
||||
// Try direct match
|
||||
List<String> 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<CompilationUnit> getCompilationUnits() {
|
||||
return Collections.unmodifiableCollection(allCus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Path> findFilesWithExtension(Set<Path> startDirs, String extension) throws RuntimeException {
|
||||
return findFilesWithExtension(startDirs, extension, Collections.emptySet());
|
||||
}
|
||||
|
||||
public static List<Path> findFilesWithExtension(Set<Path> startDirs, String extension, Set<String> ignorePatterns) throws RuntimeException {
|
||||
List<PathMatcher> matchers = ignorePatterns.stream()
|
||||
.map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
try (Stream<Path> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Transition> transitions,
|
||||
Set<String> startStates,
|
||||
Set<String> 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<Transition> transitions, Set<String> startStates, Set<String> endStates, ExportOptions options, StringBuilder sb) {
|
||||
// Choices
|
||||
Set<String> statesWithChoice = new LinkedHashSet<>();
|
||||
Map<String, String> choiceColorMap = new HashMap<>();
|
||||
@@ -187,7 +186,22 @@ public class Dot implements StateMachineExporter {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String export(String name,
|
||||
List<Transition> transitions,
|
||||
Set<String> startStates,
|
||||
Set<String> 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();
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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
|
||||
|
||||
@@ -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<String> 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<String> 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());
|
||||
|
||||
@@ -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<StateMachineExporter> exporters;
|
||||
private final EnrichmentService enrichmentService;
|
||||
|
||||
public ExportService(List<StateMachineExporter> exporters, EnrichmentService enrichmentService) {
|
||||
this.exporters = exporters;
|
||||
this.enrichmentService = enrichmentService;
|
||||
}
|
||||
|
||||
public ExportService(List<StateMachineExporter> exporters) {
|
||||
this(exporters, new EnrichmentService(List.of(
|
||||
new TriggerEnricher(),
|
||||
new EntryPointEnricher(),
|
||||
new PropertyEnricher(),
|
||||
new CallChainEnricher()
|
||||
)));
|
||||
}
|
||||
|
||||
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
|
||||
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList());
|
||||
}
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> 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<String> 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<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
Set<String> 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<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> 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<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> 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<String> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
public class PlantUmlE2ETest {
|
||||
|
||||
private final List<StateMachineExporter> 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<Path> 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<Path> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
public class RegressionTest {
|
||||
|
||||
private final List<StateMachineExporter> 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<Path> 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<Path> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, String> 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<String, String> baseProps = resolver.resolveProperties(tempDir);
|
||||
assertThat(baseProps).containsEntry("app.event", "BASE");
|
||||
|
||||
// 2. With prod profile
|
||||
resolver.setActiveProfiles(List.of("prod"));
|
||||
Map<String, String> prodProps = resolver.resolveProperties(tempDir);
|
||||
assertThat(prodProps).containsEntry("app.event", "PROD");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> activeProfiles
|
||||
) {
|
||||
public TestScenario(String name, Path inputPath, Path goldenPath, String baseName) {
|
||||
this(name, inputPath, goldenPath, baseName, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" : [ ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 171 KiB |
@@ -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
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
digraph statemachine {
|
||||
rankdir=LR;
|
||||
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||
edge [fontname="Arial", fontsize=10];
|
||||
|
||||
CHECK_AVAILABILITY_choice [shape=diamond, label="", fillcolor="blue", width=0.2, height=0.2];
|
||||
CHECK_AVAILABILITY -> CHECK_AVAILABILITY_choice [style=dotted, color="blue"];
|
||||
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||
_start -> NEW;
|
||||
CANCELLED [fillcolor=lightgray];
|
||||
DELIVERED [fillcolor=lightgray];
|
||||
RETURNED [fillcolor=lightgray];
|
||||
NEW -> CHECK_AVAILABILITY [label="PLACE_ORDER", style="solid", color="black"];
|
||||
CHECK_AVAILABILITY_choice -> PENDING_PAYMENT [label="[λ] (order=0)", style="solid", color="blue"];
|
||||
CHECK_AVAILABILITY_choice -> CANCELLED [label="(order=1)", style="solid", color="blue"];
|
||||
PENDING_PAYMENT -> PAID [label="PAY_ORDER", style="solid", color="black"];
|
||||
PAID -> SHIPPED [label="SHIP_ORDER", style="solid", color="black"];
|
||||
SHIPPED -> DELIVERED [label="FINALIZE", style="solid", color="black"];
|
||||
PAID -> CANCELLED [label="CANCEL_ORDER", style="solid", color="black"];
|
||||
DELIVERED -> RETURNED [label="RETURN_ORDER", style="solid", color="black"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "AUDIT_EVENT",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 17
|
||||
}, {
|
||||
"event" : "PLACE_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "place",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 16
|
||||
}, {
|
||||
"event" : "CANCEL_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "cancel",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 21
|
||||
}, {
|
||||
"event" : "PAY_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||
"methodName" : "processPayment",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 18
|
||||
}, {
|
||||
"event" : "SHIP_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||
"methodName" : "onShippingReady",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 17
|
||||
}, {
|
||||
"event" : "RETURN_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||
"methodName" : "onReturn",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 17
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/place",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "placeOrder",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/place",
|
||||
"verb" : "POST"
|
||||
}
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/{id}/cancel",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "cancelOrder",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/{id}/cancel",
|
||||
"verb" : "POST"
|
||||
}
|
||||
}, {
|
||||
"type" : "CUSTOM",
|
||||
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"metadata" : {
|
||||
"interceptorType" : "Spring MVC Interceptor"
|
||||
}
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/payments",
|
||||
"className" : "click.kamil.examples.enterprise.api.ReactivePaymentController",
|
||||
"methodName" : "pay",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/payments",
|
||||
"verb" : "POST"
|
||||
}
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/place",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "placeOrder",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/place",
|
||||
"verb" : "POST"
|
||||
}
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "PLACE_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "place",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 16
|
||||
}
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/{id}/cancel",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "cancelOrder",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/{id}/cancel",
|
||||
"verb" : "POST"
|
||||
}
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "CANCEL_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "cancel",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 21
|
||||
}
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "CUSTOM",
|
||||
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"metadata" : {
|
||||
"interceptorType" : "Spring MVC Interceptor"
|
||||
}
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.SecurityInterceptor.preHandle" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "AUDIT_EVENT",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 17
|
||||
}
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/payments",
|
||||
"className" : "click.kamil.examples.enterprise.api.ReactivePaymentController",
|
||||
"methodName" : "pay",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/payments",
|
||||
"verb" : "POST"
|
||||
}
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.ReactivePaymentController.pay", "click.kamil.examples.enterprise.service.ReactivePaymentService.processPayment" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "PAY_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||
"methodName" : "processPayment",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 18
|
||||
}
|
||||
} ],
|
||||
"properties" : { }
|
||||
},
|
||||
"name" : "click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "NEW" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"NEW\"",
|
||||
"fullIdentifier" : "NEW"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"CHECK_AVAILABILITY\"",
|
||||
"fullIdentifier" : "CHECK_AVAILABILITY"
|
||||
} ],
|
||||
"event" : "PLACE_ORDER",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "CHOICE",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"CHECK_AVAILABILITY\"",
|
||||
"fullIdentifier" : "CHECK_AVAILABILITY"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"PENDING_PAYMENT\"",
|
||||
"fullIdentifier" : "PENDING_PAYMENT"
|
||||
} ],
|
||||
"event" : null,
|
||||
"guard" : {
|
||||
"expression" : "(c) -> true",
|
||||
"isLambda" : true
|
||||
},
|
||||
"actions" : [ ],
|
||||
"order" : 0
|
||||
}, {
|
||||
"type" : "CHOICE",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"CHECK_AVAILABILITY\"",
|
||||
"fullIdentifier" : "CHECK_AVAILABILITY"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"CANCELLED\"",
|
||||
"fullIdentifier" : "CANCELLED"
|
||||
} ],
|
||||
"event" : null,
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : 1
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"PENDING_PAYMENT\"",
|
||||
"fullIdentifier" : "PENDING_PAYMENT"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"PAID\"",
|
||||
"fullIdentifier" : "PAID"
|
||||
} ],
|
||||
"event" : "PAY_ORDER",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"PAID\"",
|
||||
"fullIdentifier" : "PAID"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"SHIPPED\"",
|
||||
"fullIdentifier" : "SHIPPED"
|
||||
} ],
|
||||
"event" : "SHIP_ORDER",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"SHIPPED\"",
|
||||
"fullIdentifier" : "SHIPPED"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"DELIVERED\"",
|
||||
"fullIdentifier" : "DELIVERED"
|
||||
} ],
|
||||
"event" : "FINALIZE",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"PAID\"",
|
||||
"fullIdentifier" : "PAID"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"CANCELLED\"",
|
||||
"fullIdentifier" : "CANCELLED"
|
||||
} ],
|
||||
"event" : "CANCEL_ORDER",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"DELIVERED\"",
|
||||
"fullIdentifier" : "DELIVERED"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"RETURNED\"",
|
||||
"fullIdentifier" : "RETURNED"
|
||||
} ],
|
||||
"event" : "RETURN_ORDER",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"endStates" : [ "CANCELLED", "DELIVERED", "RETURNED" ]
|
||||
}
|
||||
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,78 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
hide empty description
|
||||
hide stereotype
|
||||
state NEW
|
||||
state CHECK_AVAILABILITY
|
||||
state PENDING_PAYMENT
|
||||
state CANCELLED
|
||||
state PAID
|
||||
state SHIPPED
|
||||
state DELIVERED
|
||||
state RETURNED
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
BackgroundColor LightYellow
|
||||
}
|
||||
}
|
||||
arrow {
|
||||
LineThickness 1
|
||||
.external { LineColor #1E90FF }
|
||||
.internal { LineColor #32CD32 }
|
||||
.local { LineColor #FFA500 }
|
||||
.junction { LineColor #FF69B4 }
|
||||
.join { LineColor #8A2BE2 }
|
||||
.fork { LineColor #20B2AA }
|
||||
.choice_type { LineColor #000000 }
|
||||
.choice_color_0 { LineColor #FF6347 }
|
||||
.choice_color_1 { LineColor #4682B4 }
|
||||
.choice_color_2 { LineColor #32CD32 }
|
||||
.choice_color_3 { LineColor #FFD700 }
|
||||
.choice_color_4 { LineColor #6A5ACD }
|
||||
.choice_color_5 { LineColor #FF69B4 }
|
||||
|
||||
}
|
||||
/* Example: Highlight all transitions for a specific event */
|
||||
/* .e_MY_EVENT { LineColor red } */
|
||||
</style>
|
||||
|
||||
hide <<external>> stereotype
|
||||
hide <<internal>> stereotype
|
||||
hide <<local>> stereotype
|
||||
hide <<junction>> stereotype
|
||||
hide <<join>> stereotype
|
||||
hide <<fork>> stereotype
|
||||
hide <<choice_type>> stereotype
|
||||
hide <<choice_color_0>> stereotype
|
||||
hide <<choice_color_1>> stereotype
|
||||
hide <<choice_color_2>> stereotype
|
||||
hide <<choice_color_3>> stereotype
|
||||
hide <<choice_color_4>> stereotype
|
||||
hide <<choice_color_5>> stereotype
|
||||
|
||||
[*] --> NEW
|
||||
|
||||
state CHECK_AVAILABILITY <<choice>>
|
||||
|
||||
NEW -[#1E90FF,bold]-> CHECK_AVAILABILITY <<external>> <<e_PLACE_ORDER>> : PLACE_ORDER
|
||||
CHECK_AVAILABILITY -[#FF6347,bold]-> PENDING_PAYMENT <<choice_color_0>> : [λ] (order=0)
|
||||
CHECK_AVAILABILITY -[#FF6347,bold]-> CANCELLED <<choice_color_0>> : (order=1)
|
||||
PENDING_PAYMENT -[#1E90FF,bold]-> PAID <<external>> <<e_PAY_ORDER>> : PAY_ORDER
|
||||
PAID -[#1E90FF,bold]-> SHIPPED <<external>> <<e_SHIP_ORDER>> : SHIP_ORDER
|
||||
SHIPPED -[#1E90FF,bold]-> DELIVERED <<external>> <<e_FINALIZE>> : FINALIZE
|
||||
PAID -[#1E90FF,bold]-> CANCELLED <<external>> <<e_CANCEL_ORDER>> : CANCEL_ORDER
|
||||
DELIVERED -[#1E90FF,bold]-> RETURNED <<external>> <<e_RETURN_ORDER>> : RETURN_ORDER
|
||||
hide <<e_PLACE_ORDER>> stereotype
|
||||
hide <<e_PAY_ORDER>> stereotype
|
||||
hide <<e_SHIP_ORDER>> stereotype
|
||||
hide <<e_FINALIZE>> stereotype
|
||||
hide <<e_CANCEL_ORDER>> stereotype
|
||||
hide <<e_RETURN_ORDER>> stereotype
|
||||
|
||||
CANCELLED --> [*]
|
||||
DELIVERED --> [*]
|
||||
RETURNED --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
|
||||
<state id="NEW">
|
||||
<transition target="CHECK_AVAILABILITY" event="PLACE_ORDER"/>
|
||||
</state>
|
||||
<state id="CHECK_AVAILABILITY">
|
||||
<transition target="PENDING_PAYMENT" cond="λ">
|
||||
<!-- order=0 -->
|
||||
</transition>
|
||||
<transition target="CANCELLED">
|
||||
<!-- order=1 -->
|
||||
</transition>
|
||||
</state>
|
||||
<state id="PENDING_PAYMENT">
|
||||
<transition target="PAID" event="PAY_ORDER"/>
|
||||
</state>
|
||||
<state id="CANCELLED">
|
||||
</state>
|
||||
<state id="PAID">
|
||||
<transition target="SHIPPED" event="SHIP_ORDER"/>
|
||||
<transition target="CANCELLED" event="CANCEL_ORDER"/>
|
||||
</state>
|
||||
<state id="SHIPPED">
|
||||
<transition target="DELIVERED" event="FINALIZE"/>
|
||||
</state>
|
||||
<state id="DELIVERED">
|
||||
<transition target="RETURNED" event="RETURN_ORDER"/>
|
||||
</state>
|
||||
<state id="RETURNED">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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" ]
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,70 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
hide empty description
|
||||
hide stereotype
|
||||
state START
|
||||
state PROCESSING
|
||||
state COMPLETED
|
||||
state CANCELLED
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
BackgroundColor LightYellow
|
||||
}
|
||||
}
|
||||
arrow {
|
||||
LineThickness 1
|
||||
.external { LineColor #1E90FF }
|
||||
.internal { LineColor #32CD32 }
|
||||
.local { LineColor #FFA500 }
|
||||
.junction { LineColor #FF69B4 }
|
||||
.join { LineColor #8A2BE2 }
|
||||
.fork { LineColor #20B2AA }
|
||||
.choice_type { LineColor #000000 }
|
||||
.choice_color_0 { LineColor #FF6347 }
|
||||
.choice_color_1 { LineColor #4682B4 }
|
||||
.choice_color_2 { LineColor #32CD32 }
|
||||
.choice_color_3 { LineColor #FFD700 }
|
||||
.choice_color_4 { LineColor #6A5ACD }
|
||||
.choice_color_5 { LineColor #FF69B4 }
|
||||
|
||||
}
|
||||
/* Example: Highlight all transitions for a specific event */
|
||||
/* .e_MY_EVENT { LineColor red } */
|
||||
</style>
|
||||
|
||||
hide <<external>> stereotype
|
||||
hide <<internal>> stereotype
|
||||
hide <<local>> stereotype
|
||||
hide <<junction>> stereotype
|
||||
hide <<join>> stereotype
|
||||
hide <<fork>> stereotype
|
||||
hide <<choice_type>> stereotype
|
||||
hide <<choice_color_0>> stereotype
|
||||
hide <<choice_color_1>> stereotype
|
||||
hide <<choice_color_2>> stereotype
|
||||
hide <<choice_color_3>> stereotype
|
||||
hide <<choice_color_4>> stereotype
|
||||
hide <<choice_color_5>> stereotype
|
||||
|
||||
[*] --> INIT_STATE
|
||||
|
||||
|
||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_SUBMIT_EVENT>> : SUBMIT_EVENT
|
||||
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> <<e_FINISH>> : FINISH
|
||||
PROCESSING -[#1E90FF,bold]-> CANCELLED <<external>> <<e_CANCEL_EVENT>> : CANCEL_EVENT
|
||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_REACTIVE_EVENT>> : REACTIVE_EVENT
|
||||
START -[#1E90FF,bold]-> START <<external>> <<e_AUDIT_EVENT>> : AUDIT_EVENT
|
||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_EXTERNAL_TRIGGER>> : EXTERNAL_TRIGGER
|
||||
hide <<e_SUBMIT_EVENT>> stereotype
|
||||
hide <<e_FINISH>> stereotype
|
||||
hide <<e_CANCEL_EVENT>> stereotype
|
||||
hide <<e_REACTIVE_EVENT>> stereotype
|
||||
hide <<e_AUDIT_EVENT>> stereotype
|
||||
hide <<e_EXTERNAL_TRIGGER>> stereotype
|
||||
|
||||
CANCELLED --> [*]
|
||||
COMPLETED --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="INIT_STATE">
|
||||
<state id="START">
|
||||
<transition target="PROCESSING" event="SUBMIT_EVENT"/>
|
||||
<transition target="PROCESSING" event="REACTIVE_EVENT"/>
|
||||
<transition target="START" event="AUDIT_EVENT"/>
|
||||
<transition target="PROCESSING" event="EXTERNAL_TRIGGER"/>
|
||||
</state>
|
||||
<state id="PROCESSING">
|
||||
<transition target="COMPLETED" event="FINISH"/>
|
||||
<transition target="CANCELLED" event="CANCEL_EVENT"/>
|
||||
</state>
|
||||
<state id="COMPLETED">
|
||||
</state>
|
||||
<state id="CANCELLED">
|
||||
</state>
|
||||
<state id="INIT_STATE">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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" ]
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,70 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
hide empty description
|
||||
hide stereotype
|
||||
state START
|
||||
state PROCESSING
|
||||
state COMPLETED
|
||||
state CANCELLED
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
BackgroundColor LightYellow
|
||||
}
|
||||
}
|
||||
arrow {
|
||||
LineThickness 1
|
||||
.external { LineColor #1E90FF }
|
||||
.internal { LineColor #32CD32 }
|
||||
.local { LineColor #FFA500 }
|
||||
.junction { LineColor #FF69B4 }
|
||||
.join { LineColor #8A2BE2 }
|
||||
.fork { LineColor #20B2AA }
|
||||
.choice_type { LineColor #000000 }
|
||||
.choice_color_0 { LineColor #FF6347 }
|
||||
.choice_color_1 { LineColor #4682B4 }
|
||||
.choice_color_2 { LineColor #32CD32 }
|
||||
.choice_color_3 { LineColor #FFD700 }
|
||||
.choice_color_4 { LineColor #6A5ACD }
|
||||
.choice_color_5 { LineColor #FF69B4 }
|
||||
|
||||
}
|
||||
/* Example: Highlight all transitions for a specific event */
|
||||
/* .e_MY_EVENT { LineColor red } */
|
||||
</style>
|
||||
|
||||
hide <<external>> stereotype
|
||||
hide <<internal>> stereotype
|
||||
hide <<local>> stereotype
|
||||
hide <<junction>> stereotype
|
||||
hide <<join>> stereotype
|
||||
hide <<fork>> stereotype
|
||||
hide <<choice_type>> stereotype
|
||||
hide <<choice_color_0>> stereotype
|
||||
hide <<choice_color_1>> stereotype
|
||||
hide <<choice_color_2>> stereotype
|
||||
hide <<choice_color_3>> stereotype
|
||||
hide <<choice_color_4>> stereotype
|
||||
hide <<choice_color_5>> stereotype
|
||||
|
||||
[*] --> PROD_INITIAL
|
||||
|
||||
|
||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_SUBMIT_EVENT>> : SUBMIT_EVENT
|
||||
PROCESSING -[#1E90FF,bold]-> COMPLETED <<external>> <<e_FINISH>> : FINISH
|
||||
PROCESSING -[#1E90FF,bold]-> CANCELLED <<external>> <<e_CANCEL_EVENT>> : CANCEL_EVENT
|
||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_REACTIVE_EVENT>> : REACTIVE_EVENT
|
||||
START -[#1E90FF,bold]-> START <<external>> <<e_AUDIT_EVENT>> : AUDIT_EVENT
|
||||
START -[#1E90FF,bold]-> PROCESSING <<external>> <<e_EXTERNAL_TRIGGER>> : EXTERNAL_TRIGGER
|
||||
hide <<e_SUBMIT_EVENT>> stereotype
|
||||
hide <<e_FINISH>> stereotype
|
||||
hide <<e_CANCEL_EVENT>> stereotype
|
||||
hide <<e_REACTIVE_EVENT>> stereotype
|
||||
hide <<e_AUDIT_EVENT>> stereotype
|
||||
hide <<e_EXTERNAL_TRIGGER>> stereotype
|
||||
|
||||
CANCELLED --> [*]
|
||||
COMPLETED --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="PROD_INITIAL">
|
||||
<state id="START">
|
||||
<transition target="PROCESSING" event="SUBMIT_EVENT"/>
|
||||
<transition target="PROCESSING" event="REACTIVE_EVENT"/>
|
||||
<transition target="START" event="AUDIT_EVENT"/>
|
||||
<transition target="PROCESSING" event="EXTERNAL_TRIGGER"/>
|
||||
</state>
|
||||
<state id="PROCESSING">
|
||||
<transition target="COMPLETED" event="FINISH"/>
|
||||
<transition target="CANCELLED" event="CANCEL_EVENT"/>
|
||||
</state>
|
||||
<state id="COMPLETED">
|
||||
</state>
|
||||
<state id="CANCELLED">
|
||||
</state>
|
||||
<state id="PROD_INITIAL">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -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" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 210 KiB |
@@ -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
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.F2StateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -788,7 +798,5 @@
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"endStates" : [ "States.STATEZ" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "States.STATEZ" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 249 KiB |
@@ -1,4 +1,36 @@
|
||||
@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_1
|
||||
state STATE_EXTRA_1_2
|
||||
state STATE_EXTRA_1_3
|
||||
state STATE_EXTRA_1
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.forkjoin.ForkJoinStateMachineConfig",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "States.START" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -91,7 +101,5 @@
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "States.START" ],
|
||||
"endStates" : [ "States.END" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "States.END" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,4 +1,16 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
hide empty description
|
||||
hide stereotype
|
||||
state START
|
||||
state FORK
|
||||
state REGION1_STATE1
|
||||
state REGION2_STATE1
|
||||
state REGION1_STATE2
|
||||
state REGION2_STATE2
|
||||
state JOIN
|
||||
state END
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.G1StateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -363,7 +373,5 @@
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"endStates" : [ "States.STATEZ" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "States.STATEZ" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 90 KiB |
@@ -1,4 +1,24 @@
|
||||
@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 CANCEL
|
||||
state STATEY
|
||||
state STATEX
|
||||
state STATEZ
|
||||
state STATE_EXTRA_2
|
||||
state STATE_EXTRA_3
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.G2StateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -377,7 +387,5 @@
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"endStates" : [ "States.STATEZ" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "States.STATEZ" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 98 KiB |
@@ -1,4 +1,24 @@
|
||||
@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 CANCEL
|
||||
state STATEY
|
||||
state STATEX
|
||||
state STATEZ
|
||||
state STATE_EXTRA_1
|
||||
state STATE_EXTRA_2
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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 -> START;
|
||||
WORKING [fillcolor=lightgray];
|
||||
START -> WORKING [label="INHERITED_SUBMIT", style="solid", color="black"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "INHERITED_SUBMIT",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl",
|
||||
"methodName" : "doProcess",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 15
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/v2/orders/submit",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl",
|
||||
"methodName" : "submitOrder",
|
||||
"metadata" : {
|
||||
"path" : "/api/v2/orders/submit",
|
||||
"verb" : "POST"
|
||||
}
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/v2/orders/submit",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl",
|
||||
"methodName" : "submitOrder",
|
||||
"metadata" : {
|
||||
"path" : "/api/v2/orders/submit",
|
||||
"verb" : "POST"
|
||||
}
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl.submitOrder", "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl.doProcess" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "INHERITED_SUBMIT",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl",
|
||||
"methodName" : "doProcess",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 15
|
||||
}
|
||||
} ],
|
||||
"properties" : { }
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.inheritance.config.InheritanceStateMachineConfig",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "START" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"START\"",
|
||||
"fullIdentifier" : "START"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"WORKING\"",
|
||||
"fullIdentifier" : "WORKING"
|
||||
} ],
|
||||
"event" : "INHERITED_SUBMIT",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"endStates" : [ "WORKING" ]
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,57 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
hide empty description
|
||||
hide stereotype
|
||||
state START
|
||||
state WORKING
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
BackgroundColor LightYellow
|
||||
}
|
||||
}
|
||||
arrow {
|
||||
LineThickness 1
|
||||
.external { LineColor #1E90FF }
|
||||
.internal { LineColor #32CD32 }
|
||||
.local { LineColor #FFA500 }
|
||||
.junction { LineColor #FF69B4 }
|
||||
.join { LineColor #8A2BE2 }
|
||||
.fork { LineColor #20B2AA }
|
||||
.choice_type { LineColor #000000 }
|
||||
.choice_color_0 { LineColor #FF6347 }
|
||||
.choice_color_1 { LineColor #4682B4 }
|
||||
.choice_color_2 { LineColor #32CD32 }
|
||||
.choice_color_3 { LineColor #FFD700 }
|
||||
.choice_color_4 { LineColor #6A5ACD }
|
||||
.choice_color_5 { LineColor #FF69B4 }
|
||||
|
||||
}
|
||||
/* Example: Highlight all transitions for a specific event */
|
||||
/* .e_MY_EVENT { LineColor red } */
|
||||
</style>
|
||||
|
||||
hide <<external>> stereotype
|
||||
hide <<internal>> stereotype
|
||||
hide <<local>> stereotype
|
||||
hide <<junction>> stereotype
|
||||
hide <<join>> stereotype
|
||||
hide <<fork>> stereotype
|
||||
hide <<choice_type>> stereotype
|
||||
hide <<choice_color_0>> stereotype
|
||||
hide <<choice_color_1>> stereotype
|
||||
hide <<choice_color_2>> stereotype
|
||||
hide <<choice_color_3>> stereotype
|
||||
hide <<choice_color_4>> stereotype
|
||||
hide <<choice_color_5>> stereotype
|
||||
|
||||
[*] --> START
|
||||
|
||||
|
||||
START -[#1E90FF,bold]-> WORKING <<external>> <<e_INHERITED_SUBMIT>> : INHERITED_SUBMIT
|
||||
hide <<e_INHERITED_SUBMIT>> stereotype
|
||||
|
||||
WORKING --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="START">
|
||||
<state id="START">
|
||||
<transition target="WORKING" event="INHERITED_SUBMIT"/>
|
||||
</state>
|
||||
<state id="WORKING">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.enumstate.KamilEnumStateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "OrderStates.SUBMITTED" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -195,7 +205,5 @@
|
||||
} ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "OrderStates.SUBMITTED" ],
|
||||
"endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 36 KiB |
@@ -1,4 +1,16 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
hide empty description
|
||||
hide stereotype
|
||||
state SUBMITTED
|
||||
state PAID
|
||||
state FULFILLED
|
||||
state CANCELED
|
||||
state PAID1
|
||||
state PAID2
|
||||
state PAID3
|
||||
state HAPPEN
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.inheritancestate.OneStateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -631,7 +641,5 @@
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"endStates" : [ "States.STATEZ" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "States.STATEZ" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 190 KiB |
@@ -1,4 +1,32 @@
|
||||
@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 STATEY
|
||||
state STATEX
|
||||
state STATEZ
|
||||
state STATE17
|
||||
state STATE18
|
||||
state STATE19
|
||||
state STATE20
|
||||
state STATE_EXTRA_1
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "OrderStates.SUBMITTED" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -212,7 +222,5 @@
|
||||
} ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "OrderStates.SUBMITTED" ],
|
||||
"endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 44 KiB |
@@ -1,4 +1,16 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
hide empty description
|
||||
hide stereotype
|
||||
state SUBMITTED
|
||||
state PAID
|
||||
state FULFILLED
|
||||
state CANCELED
|
||||
state PAID2
|
||||
state PAID3
|
||||
state PAID1
|
||||
state HAPPEN
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.ThreeStateMachineConfiguration",
|
||||
"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" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 210 KiB |
@@ -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
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"spring.application.name" : "statemachinedemo"
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.TwoStateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
@@ -729,7 +739,5 @@
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "States.STATE1" ],
|
||||
"endStates" : [ "States.STATEZ" ],
|
||||
"renderChoicesAsDiamonds" : true
|
||||
"endStates" : [ "States.STATEZ" ]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 219 KiB |
@@ -1,4 +1,33 @@
|
||||
@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
|
||||
|
||||
<style>
|
||||
state {
|
||||
.choice {
|
||||
|
||||
30
state_machines/enterprise_order_system/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.5.3'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
}
|
||||
|
||||
group = 'click.kamil'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-artemis' // For JMS
|
||||
implementation 'org.springframework.boot:spring-boot-starter-amqp' // For RabbitMQ
|
||||
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package click.kamil.examples.enterprise.api;
|
||||
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@RequestMapping("/api/enterprise/orders")
|
||||
public interface OrderApi {
|
||||
|
||||
@PostMapping("/place")
|
||||
void placeOrder();
|
||||
|
||||
@PostMapping("/{id}/cancel")
|
||||
void cancelOrder(@PathVariable("id") String id);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package click.kamil.examples.enterprise.api;
|
||||
|
||||
import click.kamil.examples.enterprise.service.OrderService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class OrderController implements OrderApi {
|
||||
|
||||
private final OrderService orderService;
|
||||
|
||||
@Override
|
||||
public void placeOrder() {
|
||||
orderService.place();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelOrder(String id) {
|
||||
orderService.cancel(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package click.kamil.examples.enterprise.api;
|
||||
|
||||
import click.kamil.examples.enterprise.service.ReactivePaymentService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class ReactivePaymentController {
|
||||
|
||||
private final ReactivePaymentService paymentService;
|
||||
|
||||
@PostMapping("/api/enterprise/payments")
|
||||
public Mono<Void> pay(@RequestBody String orderId) {
|
||||
return paymentService.processPayment(orderId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package click.kamil.examples.enterprise.api;
|
||||
|
||||
import click.kamil.examples.enterprise.constants.OrderEvents;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final StateMachine<String, String> stateMachine;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
stateMachine.sendEvent(OrderEvents.AUDIT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package click.kamil.examples.enterprise.config;
|
||||
|
||||
import click.kamil.examples.enterprise.constants.OrderEvents;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.statemachine.config.EnableStateMachine;
|
||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine(name = "enterpriseStateMachine")
|
||||
public class EnterpriseStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
|
||||
|
||||
@Value("${order.initial.state:NEW}")
|
||||
private String initialState;
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(initialState)
|
||||
.state("PENDING_PAYMENT")
|
||||
.state("PAID")
|
||||
.state("SHIPPED")
|
||||
.choice("CHECK_AVAILABILITY")
|
||||
.end("CANCELLED")
|
||||
.end("RETURNED")
|
||||
.end("DELIVERED");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source("NEW").target("CHECK_AVAILABILITY")
|
||||
.event(OrderEvents.PLACE)
|
||||
.and()
|
||||
.withChoice()
|
||||
.source("CHECK_AVAILABILITY")
|
||||
.first("PENDING_PAYMENT", (c) -> true)
|
||||
.last("CANCELLED")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("PENDING_PAYMENT").target("PAID")
|
||||
.event(OrderEvents.PAY)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("PAID").target("SHIPPED")
|
||||
.event(OrderEvents.SHIP)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("SHIPPED").target("DELIVERED")
|
||||
.event("FINALIZE")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("PAID").target("CANCELLED")
|
||||
.event(OrderEvents.CANCEL)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("DELIVERED").target("RETURNED")
|
||||
.event(OrderEvents.RETURN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package click.kamil.examples.enterprise.constants;
|
||||
|
||||
public class OrderEvents {
|
||||
public static final String PLACE = "PLACE_ORDER";
|
||||
public static final String PAY = "PAY_ORDER";
|
||||
public static final String SHIP = "SHIP_ORDER";
|
||||
public static final String CANCEL = "CANCEL_ORDER";
|
||||
public static final String RETURN = "RETURN_ORDER";
|
||||
public static final String AUDIT = "AUDIT_EVENT";
|
||||
}
|
||||