extended analysis

This commit is contained in:
2026-06-14 16:32:51 +02:00
parent 2251a587d9
commit ab474c0766
125 changed files with 4180 additions and 317 deletions

View File

@@ -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.

View File

@@ -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'

View File

@@ -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"];
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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) {}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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,6 +186,21 @@ 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();

View File

@@ -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

View File

@@ -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());

View File

@@ -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 = AnalysisResult.builder()
.name(model.getName())
.transitions(model.getTransitions())
.startStates(model.getStartStates())
.endStates(model.getEndStates())
.renderChoicesAsDiamonds(model.isRenderChoicesAsDiamonds())
.build();
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
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);

View File

@@ -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);
}
}

View File

@@ -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();
List<Path> pumlFiles;
try (var stream = Files.list(actualDir)) {
pumlFiles = stream.filter(p -> p.toString().endsWith(".puml")).toList();
}
// 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();
for (Path pumlFile : pumlFiles) {
String fileName = pumlFile.getFileName().toString();
String baseNoExt = fileName.substring(0, fileName.lastIndexOf('.'));
String goldenBaseNoExt = baseNoExt.replace(actualBaseName, targetBaseName);
// 3. Compare with Golden
Path goldenPngFile = scenario.goldenPath().resolve(targetBaseName + ".png");
// 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();
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();
// 3. Compare with Golden
Path goldenPngFile = scenario.goldenPath().resolve(goldenBaseNoExt + ".png");
byte[] expectedPngBytes = Files.readAllBytes(goldenPngFile);
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();
assertThat(actualPngBytes)
.as("Visual regression detected for %s. PNG output does not match golden reference.", scenario.name())
.containsExactly(expectedPngBytes);
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);
}
}
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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());
}
}

View File

@@ -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" : [ ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -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 {

View File

@@ -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"];
}

View File

@@ -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" ]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -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

View File

@@ -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>

View File

@@ -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"];
}

View File

@@ -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" ]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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

View File

@@ -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>

View File

@@ -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"];
}

View File

@@ -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" ]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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

View File

@@ -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>

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 249 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -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 {

View File

@@ -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"];
}

View File

@@ -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" ]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -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

View File

@@ -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>

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -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 {

View File

@@ -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" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 219 KiB

View File

@@ -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 {

View 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'
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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";
}

Some files were not shown because too many files have changed in this diff Show More