Fix golden audit issues and tighten analysis/export pipeline.

Resolve property placeholders before call-chain linking, skip phantom routing states and lifecycle triggers from transition matching, dedupe exported states, and add golden JSON audit regression plus HTML lifecycle endpoint display.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 05:49:25 +02:00
parent 4f13c9449c
commit b158179559
53 changed files with 882 additions and 2057 deletions

View File

@@ -46,6 +46,7 @@ HTML-specific options:
| `--no-diamonds` | Render choice pseudostates as normal nodes |
| `--debug` | Same diagnostic mode as the core exporter |
| `--include-generated-sources` | Same generated-source scanning as the core exporter |
| `--no-inline-accessors` | Disable accessor indexing/inlining (enabled by default) |
| `-p, --profiles` | Spring profiles for property resolution |
## Business Flows

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""Deep audit of golden JSON analysis exports."""
import json
import re
from collections import defaultdict
from pathlib import Path
GOLDEN = Path(__file__).resolve().parent.parent / "src/test/resources/golden"
PH = re.compile(r"\$\{")
def fn_form(identifier):
if not identifier:
return identifier
last = identifier.rfind(".")
if last <= 0:
return identifier
prev = identifier.rfind(".", 0, last)
return identifier[prev + 1 :] if prev > 0 else identifier
def sid(st):
if isinstance(st, dict):
return st.get("fullIdentifier") or st.get("rawName")
return st
def eid(ev):
if isinstance(ev, dict):
return ev.get("fullIdentifier") or ev.get("rawName")
return ev
def audit_file(path, data):
name = data.get("name", path.stem)
issues = []
st_fqn = data.get("stateTypeFqn")
ev_fqn = data.get("eventTypeFqn")
transitions = data.get("transitions") or []
starts = list(data.get("startStates") or [])
ends = list(data.get("endStates") or [])
states = data.get("states") or []
meta = data.get("metadata") or {}
is_pkg_enum = st_fqn and "." in st_fqn and st_fqn not in ("String", "java.lang.String")
is_string = st_fqn in (None, "String", "java.lang.String") or (
not st_fqn and not ev_fqn
)
if PH.search(json.dumps(data)):
issues.append("BUG: unresolved ${placeholder} in export")
# Duplicate fullIdentifiers in states[]
by_full = defaultdict(list)
for s in states:
by_full[sid(s)].append(s.get("rawName"))
for fid, raws in by_full.items():
if fid and len(raws) > 1 and len(set(raws)) > 1:
issues.append(f"NOISE: duplicate state entry {fid!r} with rawNames {raws}")
# Collect transition state/event ids
t_events, t_sources, t_targets = set(), set(), set()
for t in transitions:
if eid(t.get("event")):
t_events.add(eid(t.get("event")))
for s in t.get("sourceStates") or []:
t_sources.add(sid(s))
for s in t.get("targetStates") or []:
t_targets.add(sid(s))
graph = t_sources | t_targets
# startStates canonical consistency
for ss in starts:
if is_string and ss and not ss.startswith("String."):
if any(x.startswith("String.") for x in graph):
issues.append(f"BUG: startStates {ss!r} bare but transitions use String.* form")
if ss not in graph and fn_form(ss) not in {fn_form(x) for x in graph}:
issues.append(f"WARN: startStates {ss!r} not in transition graph")
# endStates
for es in ends:
if es not in graph and fn_form(es) not in {fn_form(x) for x in graph}:
issues.append(f"WARN: endStates {es!r} not in transition graph")
# Enum canonical fullIdentifiers
if is_pkg_enum:
for i, t in enumerate(transitions):
for side in ("sourceStates", "targetStates"):
for s in t.get(side) or []:
fid = sid(s)
if fid and not fid.startswith(st_fqn + ".") and not fid.startswith('"'):
if "." in fid and not fid.startswith("<"):
issues.append(f"BUG: transitions[{i}].{side} {fid!r} not under {st_fqn}")
ev = eid(t.get("event"))
if ev and ev_fqn and "." in ev_fqn and not ev.startswith("<") and not ev.startswith(ev_fqn + "."):
if ev not in ("event",) and "." in ev and not ev.startswith(ev_fqn):
issues.append(f"BUG: transitions[{i}].event {ev!r} not under {ev_fqn}")
for i, tr in enumerate(meta.get("triggers") or []):
for field in ("event", "sourceState"):
val = tr.get(field)
if not val or val in ("event", "payload", "customMessage", "eventProvider"):
continue
type_fqn = ev_fqn if field == "event" else st_fqn
if type_fqn and "." in val and not val.startswith("<") and not val.startswith(type_fqn + "."):
if field == "sourceState" and val not in graph and fn_form(val) not in {fn_form(x) for x in graph}:
issues.append(f"BUG: triggers[{i}].sourceState {val!r} not a machine state")
elif field == "event" and type_fqn and val.isupper() is False and "." not in val and not val.startswith("<"):
if val not in ("event", "payload", "customMessage", "eventProvider", "true", "false"):
issues.append(f"INFO: triggers[{i}].event dynamic {val!r}")
# matchedTransitions
for i, chain in enumerate(meta.get("callChains") or []):
for j, mt in enumerate(chain.get("matchedTransitions") or []):
me, ms, mtgt = mt.get("event"), mt.get("sourceState"), mt.get("targetState")
ok = False
for t in transitions:
if fn_form(eid(t.get("event"))) != fn_form(me):
continue
srcs = [sid(s) for s in t.get("sourceStates") or []]
tgts = [sid(s) for s in t.get("targetStates") or []]
if (not ms or any(fn_form(x) == fn_form(ms) for x in srcs)) and (
not mtgt or any(fn_form(x) == fn_form(mtgt) for x in tgts)
):
ok = True
break
if not ok:
issues.append(
f"BUG: callChains[{i}].matchedTransitions[{j}] ({ms!r},{me!r},{mtgt!r}) no transition"
)
# String machine: mixed NEW vs String.NEW
if is_string or st_fqn == "String":
all_ids = set(graph) | set(starts) | set(ends) | {sid(s) for s in states}
for x in list(all_ids):
if not x:
continue
bare = x.strip('"')
if bare and "." not in bare and f"String.{bare}" in all_ids:
issues.append(f"BUG: mixed bare {x!r} and String.{bare}")
# stateTypeFqn missing on enum machine
if is_pkg_enum and not st_fqn:
issues.append("BUG: enum machine missing stateTypeFqn")
return name, issues
def main():
total = 0
for path in sorted(GOLDEN.rglob("*.json")):
data = json.loads(path.read_text())
machine, issues = audit_file(path, data)
rel = path.relative_to(GOLDEN)
bugs = [i for i in issues if i.startswith("BUG")]
warns = [i for i in issues if i.startswith("WARN")]
noise = [i for i in issues if i.startswith("NOISE")]
info = [i for i in issues if i.startswith("INFO")]
if bugs or warns:
print(f"\n{'='*70}\n{rel}\n machine: {machine}")
for i in bugs + warns + noise + info:
print(f" [{i.split(':')[0]}] {i.split(': ', 1)[-1]}")
total += len(bugs) + len(warns)
print(f"\nTotal BUG+WARN: {total}")
if __name__ == "__main__":
main()

View File

@@ -4,6 +4,7 @@ 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.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
@@ -42,6 +43,11 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
continue;
}
if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
updatedChains.add(chain);
continue;
}
String triggerEvent = simplify(tp.getEvent());
String triggerSource = tp.getSourceState() != null ? simplify(tp.getSourceState()) : null;

View File

@@ -12,6 +12,11 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
return false;
}
if (click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers
.isLifecycle(triggerPoint.getEvent())) {
return false;
}
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
String rawTriggerEvent = triggerPoint.getEvent();

View File

@@ -0,0 +1,36 @@
package click.kamil.springstatemachineexporter.analysis.model;
/**
* Synthetic trigger markers for persistence/lifecycle operations that do not fire
* {@code sendEvent} transitions (e.g. {@code StateMachinePersister.restore(...)}).
*/
public final class LifecycleTriggerMarkers {
public static final String PREFIX = "[LIFECYCLE:";
private LifecycleTriggerMarkers() {
}
public static boolean isLifecycle(String event) {
return event != null && event.startsWith(PREFIX) && event.endsWith("]");
}
public static String lifecycleType(String event) {
if (!isLifecycle(event)) {
return null;
}
return event.substring(PREFIX.length(), event.length() - 1);
}
public static String displayLabel(String event) {
String type = lifecycleType(event);
if (type == null) {
return event;
}
return switch (type.toUpperCase()) {
case "RESTORE" -> "Restore persisted state";
case "RESET" -> "Reset state machine";
default -> "Lifecycle: " + type.toLowerCase();
};
}
}

View File

@@ -1,17 +1,77 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
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.TriggerCanonicalizationEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.RequiredArgsConstructor;
import java.util.List;
@RequiredArgsConstructor
public class EnrichmentService {
private final List<AnalysisEnricher> enrichers;
private final List<AnalysisEnricher> prePropertyEnrichers;
private final List<AnalysisEnricher> postPropertyEnrichers;
/** Single-phase pipeline (all enrichers run together; used by focused tests). */
public EnrichmentService(List<AnalysisEnricher> enrichers) {
this(enrichers, List.of());
}
public EnrichmentService(
List<AnalysisEnricher> prePropertyEnrichers,
List<AnalysisEnricher> postPropertyEnrichers) {
this.prePropertyEnrichers = prePropertyEnrichers;
this.postPropertyEnrichers = postPropertyEnrichers;
}
public static EnrichmentService createDefault() {
return new EnrichmentService(
List.of(
new TriggerEnricher(),
new EntryPointEnricher(),
new PropertyEnricher()),
List.of(
new CallChainEnricher(),
new ForwardPathEstimationEnricher(),
new TriggerCanonicalizationEnricher(),
new TransitionLinkerEnricher()));
}
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
enrichPreProperty(result, context, intelligence);
enrichPostProperty(result, context, intelligence);
}
public void enrichPreProperty(
AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
runEnrichers(prePropertyEnrichers, result, context, intelligence);
}
public void enrichPostProperty(
AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
runEnrichers(postPropertyEnrichers, result, context, intelligence);
}
/** Re-runs transition linking after property resolution without rebuilding call chains. */
public void relinkAfterPropertyResolution(
AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
runEnrichers(
List.of(new TriggerCanonicalizationEnricher(), new TransitionLinkerEnricher()),
result,
context,
intelligence);
}
private static void runEnrichers(
List<AnalysisEnricher> enrichers,
AnalysisResult result,
CodebaseContext context,
CodebaseIntelligenceProvider intelligence) {
for (AnalysisEnricher enricher : enrichers) {
enricher.enrich(result, context, intelligence);
}

View File

@@ -11,6 +11,7 @@ import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@Slf4j
@RequiredArgsConstructor
@@ -19,6 +20,17 @@ public class GenericEventDetector {
private final ConstantResolver constantResolver;
private final List<LibraryHint> hints;
/** Parameter names that route to a machine, not SM state enum constants. */
private static final Set<String> ROUTING_PARAMETER_NAMES = Set.of(
"machineType",
"machineId",
"machineName",
"domain",
"type",
"action",
"eventString",
"version");
public List<TriggerPoint> detect(CompilationUnit cu) {
List<TriggerPoint> triggers = new ArrayList<>();
String fileName = "unknown";
@@ -296,8 +308,10 @@ public class GenericEventDetector {
String state = extractStateFromSiblings(current, block.statements());
if (state != null) return state;
} else if (parent instanceof SwitchStatement switchStmt) {
String state = extractStateFromSiblings(current, switchStmt.statements());
if (state != null) return state;
if (!isRoutingParameter(switchStmt.getExpression())) {
String state = extractStateFromSiblings(current, switchStmt.statements());
if (state != null) return state;
}
}
current = parent;
@@ -354,6 +368,10 @@ public class GenericEventDetector {
if (left instanceof NullLiteral || right instanceof NullLiteral) {
return null;
}
if (isRoutingParameter(left) || isRoutingParameter(right)) {
return null;
}
// Usually one is a method call like getState() or a variable like `state`
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
@@ -374,6 +392,10 @@ public class GenericEventDetector {
if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
Expression receiver = mi.getExpression();
Expression arg = (Expression) mi.arguments().get(0);
if (isRoutingParameter(receiver) || isRoutingParameter(arg)) {
return null;
}
// If receiver is null (e.g., implicit this), fall back to arg
if (receiver == null) {
@@ -395,6 +417,13 @@ public class GenericEventDetector {
return null;
}
private boolean isRoutingParameter(Expression expr) {
if (expr instanceof SimpleName sn) {
return ROUTING_PARAMETER_NAMES.contains(sn.getIdentifier());
}
return false;
}
private String getSimpleNameString(Expression expr) {
if (expr instanceof QualifiedName qn) {
return qn.getName().getIdentifier();

View File

@@ -45,20 +45,44 @@ public class TransitionStateUtils {
}
public static Set<State> findAllStates(Collection<Transition> transitions, Set<String> initialStates, Set<String> endStates) {
Set<State> allStates = new java.util.HashSet<>();
java.util.Map<String, State> byFullIdentifier = new java.util.LinkedHashMap<>();
if (transitions != null) {
transitions.forEach(t -> {
if (t.getSourceStates() != null) allStates.addAll(t.getSourceStates());
if (t.getTargetStates() != null) allStates.addAll(t.getTargetStates());
mergeState(byFullIdentifier, t.getSourceStates());
mergeState(byFullIdentifier, t.getTargetStates());
});
}
if (initialStates != null) {
initialStates.forEach(s -> allStates.add(State.of(s)));
initialStates.forEach(label -> mergeState(byFullIdentifier, State.of(label)));
}
if (endStates != null) {
endStates.forEach(s -> allStates.add(State.of(s)));
endStates.forEach(label -> mergeState(byFullIdentifier, State.of(label)));
}
return allStates;
return new java.util.LinkedHashSet<>(byFullIdentifier.values());
}
private static void mergeState(java.util.Map<String, State> map, State state) {
if (state == null) {
return;
}
String key = stateKey(state);
map.putIfAbsent(key, state);
}
private static void mergeState(java.util.Map<String, State> map, java.util.Collection<State> states) {
if (states == null) {
return;
}
for (State state : states) {
mergeState(map, state);
}
}
private static String stateKey(State state) {
if (state.fullIdentifier() != null && !state.fullIdentifier().isBlank()) {
return state.fullIdentifier();
}
return state.rawName();
}
private static Stream<String> normalizeStates(Collection<State> states) {

View File

@@ -28,6 +28,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CodebaseContext {
private final Map<String, CompilationUnit> classes = new HashMap<>();
private final Map<String, org.eclipse.jdt.core.dom.RecordDeclaration> recordDeclarations = new HashMap<>();
private final Map<String, Path> classPaths = new HashMap<>();
private final Map<String, String> simpleNameToFqn = new HashMap<>();
private final Set<String> ambiguousSimpleNames = new HashSet<>();
@@ -317,6 +318,7 @@ public class CodebaseContext {
// Treat as a class since it is a type
classes.put(fqn, (CompilationUnit) rd.getRoot());
classPaths.put(fqn, javaFile);
recordDeclarations.put(fqn, rd);
if (simpleNameToFqn.containsKey(simpleName)) {
String existingFqn = simpleNameToFqn.get(simpleName);
@@ -645,6 +647,21 @@ public class CodebaseContext {
return null;
}
public org.eclipse.jdt.core.dom.RecordDeclaration getRecordDeclaration(String fqn) {
if (fqn == null || fqn.isEmpty()) {
return null;
}
org.eclipse.jdt.core.dom.RecordDeclaration indexed = recordDeclarations.get(fqn);
if (indexed != null) {
return indexed;
}
AbstractTypeDeclaration abstractType = getAbstractTypeDeclaration(fqn);
if (abstractType instanceof org.eclipse.jdt.core.dom.RecordDeclaration recordDeclaration) {
return recordDeclaration;
}
return null;
}
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name) {
// Try exact FQN match first
CompilationUnit cu = classes.get(name);

View File

@@ -745,11 +745,7 @@ public class JdtDataFlowModel implements DataFlowModel {
}
private org.eclipse.jdt.core.dom.RecordDeclaration findRecordDeclaration(String fqn) {
org.eclipse.jdt.core.dom.AbstractTypeDeclaration abstractType = context.getAbstractTypeDeclaration(fqn);
if (abstractType instanceof org.eclipse.jdt.core.dom.RecordDeclaration recordDeclaration) {
return recordDeclaration;
}
return null;
return context.getRecordDeclaration(fqn);
}
private void inlineAccessorSetter(

View File

@@ -1,11 +1,7 @@
package click.kamil.springstatemachineexporter.service;
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
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.model.BusinessFlow;
import click.kamil.springstatemachineexporter.analysis.service.AnalysisResultFinalizer;
@@ -44,15 +40,7 @@ public class ExportService {
}
public ExportService(List<StateMachineExporter> exporters) {
this(exporters, new EnrichmentService(List.of(
new TriggerEnricher(),
new EntryPointEnricher(),
new PropertyEnricher(),
new CallChainEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
)));
this(exporters, EnrichmentService.createDefault());
}
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
@@ -195,6 +183,8 @@ public class ExportService {
log.info("JSON re-export: no source project found; using embedded machine types if present");
}
enrichmentService.relinkAfterPropertyResolution(result, context, null);
StateMachineTypeResolver.MachineTypes machineTypes =
click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory.resolveMachineTypes(
result.getName(),
@@ -295,9 +285,9 @@ public class ExportService {
.flows(flows)
.build();
enrichmentService.enrich(result, context, intelligence);
enrichmentService.enrichPreProperty(result, context, intelligence);
resolveProperties(result, activeProfiles);
enrichmentService.enrichPostProperty(result, context, intelligence);
AnalysisResultFinalizer.finalizeResult(result, context);
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
@@ -337,9 +327,9 @@ public class ExportService {
.flows(flows)
.build();
enrichmentService.enrich(result, context, intelligence);
enrichmentService.enrichPreProperty(result, context, intelligence);
resolveProperties(result, activeProfiles);
enrichmentService.enrichPostProperty(result, context, intelligence);
AnalysisResultFinalizer.finalizeResult(result, context);
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);

View File

@@ -0,0 +1,50 @@
package click.kamil.springstatemachineexporter;
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Runs the golden JSON consistency audit script as a regression gate.
*/
class GoldenJsonAuditTest {
private static Path findProjectRoot() {
Path current = Path.of(".").toAbsolutePath();
while (current != null && !current.resolve("settings.gradle").toFile().exists()) {
current = current.getParent();
}
return current;
}
@Test
void goldenJsonExportsShouldPassConsistencyAudit() throws Exception {
Path moduleRoot = findProjectRoot().resolve("state_machine_exporter");
Path script = moduleRoot.resolve("scripts/audit_golden_json.py");
assertThat(script).exists();
Process process = new ProcessBuilder("python3", script.toString())
.directory(moduleRoot.toFile())
.redirectErrorStream(true)
.start();
String output;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
output = reader.lines().collect(Collectors.joining("\n"));
}
assertThat(process.waitFor(60, TimeUnit.SECONDS))
.as("audit script timed out\n%s", output)
.isTrue();
assertThat(process.exitValue())
.as("audit script failed\n%s", output)
.isZero();
assertThat(output).contains("Total BUG+WARN: 0");
}
}

View File

@@ -1,6 +1,5 @@
package click.kamil.springstatemachineexporter;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
import click.kamil.springstatemachineexporter.exporter.Dot;
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.exporter.PlantUml;
@@ -17,7 +16,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
public class GoldenUpdater {
@@ -33,7 +31,16 @@ public class GoldenUpdater {
System.out.println("Updating golden files for: " + scenario.name());
Path tempDir = Files.createTempDirectory("golden-update-");
try {
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true);
exportService.runExporter(
scenario.inputPath(),
tempDir,
List.of("puml", "dot", "scxml", "json"),
true,
scenario.activeProfiles(),
null,
null,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
List<Path> generatedDirs;
try (var stream = Files.list(tempDir)) {
@@ -159,12 +166,73 @@ public class GoldenUpdater {
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
"G2StateMachineConfiguration"
),
new TestScenario(
"Extended Analysis Sample",
Path.of("state_machines/extended_analysis_sample"),
Path.of("src/test/resources/golden/ExtendedStateMachineConfig"),
"ExtendedStateMachineConfig"
),
new TestScenario(
"Extended Analysis Sample (PROD)",
Path.of("state_machines/extended_analysis_sample"),
Path.of("src/test/resources/golden/ExtendedStateMachineConfig_PROD"),
"ExtendedStateMachineConfig",
List.of("prod")
),
new TestScenario(
"Inheritance Sample",
Path.of("state_machines/inheritance_sample"),
Path.of("src/test/resources/golden/InheritanceStateMachineConfig"),
"InheritanceStateMachineConfig"
),
new TestScenario(
"Enterprise Order System",
Path.of("state_machines/enterprise_order_system"),
Path.of("src/test/resources/golden/EnterpriseStateMachineConfig"),
"EnterpriseStateMachineConfig"
),
new TestScenario(
"Enterprise Order State Machine",
Path.of("state_machines/state_machine_enterprise"),
Path.of("src/test/resources/golden/OrderStateMachineConfiguration"),
"OrderStateMachineConfiguration"
),
new TestScenario(
"Multi-Module Sample",
Path.of("state_machines/multi_module_sample/core-module"),
Path.of("src/test/resources/golden/OrderStateMachineConfig"),
"OrderStateMachineConfig"
),
new TestScenario(
"Maven Multi-Module Sample",
Path.of("state_machines/maven_multi_module/core-module"),
Path.of("src/test/resources/golden/MavenOrderStateMachine"),
"MavenOrderStateMachine"
),
new TestScenario(
"Complex Multi-Module Sample",
Path.of("state_machines/complex_multi_module_sm"),
Path.of("src/test/resources/golden/StateMachineConfig"),
"StateMachineConfig"
),
new TestScenario(
"Polymorphic Events Sample",
Path.of("state_machines/polymorphic_events_sample"),
Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"),
"PolymorphicStateMachineConfiguration"
),
new TestScenario(
"Layered Dispatcher (Order)",
Path.of("state_machines/layered_dispatcher_sample"),
Path.of("src/test/resources/golden/StandardOrderStateMachineConfiguration"),
"StandardOrderStateMachineConfiguration"
),
new TestScenario(
"Layered Dispatcher (Document)",
Path.of("state_machines/layered_dispatcher_sample"),
Path.of("src/test/resources/golden/StandardDocumentStateMachineConfiguration"),
"StandardDocumentStateMachineConfiguration"
),
new TestScenario(
"Enterprise Document State Machine",
Path.of("state_machines/state_machine_enterprise"),

View File

@@ -173,6 +173,18 @@ public class RegressionTest {
root.resolve("state_machines/layered_dispatcher_sample"),
Path.of("src/test/resources/golden/StandardDocumentStateMachineConfiguration"),
"StandardDocumentStateMachineConfiguration"
),
new TestScenario(
"Enterprise Document State Machine",
root.resolve("state_machines/state_machine_enterprise"),
Path.of("src/test/resources/golden/DocumentStateMachineConfiguration"),
"DocumentStateMachineConfiguration"
),
new TestScenario(
"Enterprise User State Machine",
root.resolve("state_machines/state_machine_enterprise"),
Path.of("src/test/resources/golden/UserStateMachineConfiguration"),
"UserStateMachineConfiguration"
)
);
}

View File

@@ -0,0 +1,23 @@
package click.kamil.springstatemachineexporter.analysis.model;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class LifecycleTriggerMarkersTest {
@Test
void shouldRecognizeLifecycleMarkers() {
assertThat(LifecycleTriggerMarkers.isLifecycle("[LIFECYCLE:RESTORE]")).isTrue();
assertThat(LifecycleTriggerMarkers.isLifecycle("[LIFECYCLE:RESET]")).isTrue();
assertThat(LifecycleTriggerMarkers.isLifecycle("OrderEvent.PAY")).isFalse();
}
@Test
void shouldFormatDisplayLabels() {
assertThat(LifecycleTriggerMarkers.displayLabel("[LIFECYCLE:RESTORE]"))
.isEqualTo("Restore persisted state");
assertThat(LifecycleTriggerMarkers.displayLabel("[LIFECYCLE:RESET]"))
.isEqualTo("Reset state machine");
}
}

View File

@@ -129,4 +129,33 @@ class GenericEventDetectorControlFlowTest {
TriggerPoint shipTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.SHIP")).findFirst().get();
assertThat(shipTrigger.getSourceState()).isEqualTo("PAID");
}
@Test
void shouldNotTreatMachineTypeDiscriminatorAsSourceState(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class Dispatcher {
private StateMachine sm;
public void dispatch(String machineType, String eventString) {
if ("ORDER".equalsIgnoreCase(machineType)) {
sm.sendEvent("event");
}
}
}
class StateMachine {
public void sendEvent(String e) {}
}
""";
Files.writeString(tempDir.resolve("Dispatcher.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
List<TriggerPoint> triggers = detector.detect(
(org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.Dispatcher").getRoot());
assertThat(triggers).hasSize(1);
assertThat(triggers.get(0).getSourceState()).isNull();
}
}

View File

@@ -0,0 +1,52 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class LifecycleDetectorTest {
@Test
void shouldDetectPersisterRestoreCalls(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.StateMachinePersister;
public class OrderService {
private final StateMachine<String, String> stateMachine;
private final StateMachinePersister<String, String, String> persister;
OrderService(StateMachine<String, String> sm, StateMachinePersister<String, String, String> p) {
this.stateMachine = sm;
this.persister = p;
}
public void resume(String id) throws Exception {
persister.restore(stateMachine, id);
}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
LifecycleDetector detector = new LifecycleDetector(context);
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderService");
org.eclipse.jdt.core.dom.CompilationUnit cu =
(org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot();
List<TriggerPoint> triggers = detector.detect(cu);
assertThat(triggers).hasSize(1);
assertThat(triggers.get(0).getEvent()).isEqualTo("[LIFECYCLE:RESTORE]");
assertThat(triggers.get(0).getMethodName()).isEqualTo("resume");
assertThat(LifecycleTriggerMarkers.lifecycleType(triggers.get(0).getEvent())).isEqualTo("RESTORE");
}
}

View File

@@ -217,6 +217,24 @@ class TransitionStateUtilsTest {
assertThat(endStates).containsExactlyInAnyOrder("DELIVERED", "REFUNDED");
}
@Test
void shouldDeduplicateStatesByFullIdentifier() {
Transition transition = new Transition();
transition.getSourceStates().add(State.of("\"NEW\"", "String.NEW"));
transition.getTargetStates().add(State.of("\"PAID\"", "String.PAID"));
Set<State> allStates = TransitionStateUtils.findAllStates(
List.of(transition),
Set.of("String.NEW"),
Set.of("String.PAID"));
assertThat(allStates).hasSize(2);
assertThat(allStates).extracting(State::fullIdentifier)
.containsExactlyInAnyOrder("String.NEW", "String.PAID");
assertThat(allStates).extracting(State::rawName)
.containsExactlyInAnyOrder("\"NEW\"", "\"PAID\"");
}
private Transition createTransition(String source, String target) {
Transition transition = new Transition();
if (source != null) {

View File

@@ -30,9 +30,6 @@
}, {
"rawName" : "States.STATE20",
"fullIdentifier" : "ComplexStateMachineConfig.States.STATE20"
}, {
"rawName" : "ComplexStateMachineConfig.States.STATE1",
"fullIdentifier" : "ComplexStateMachineConfig.States.STATE1"
}, {
"rawName" : "States.STATE1",
"fullIdentifier" : "ComplexStateMachineConfig.States.STATE1"

View File

@@ -1,33 +1,76 @@
{
"name" : "click.kamil.enterprise.machines.document.DocumentStateMachineConfiguration",
"states" : [ {
"rawName" : "DocumentState.IN_REVIEW",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW"
}, {
"rawName" : "DocumentState.DRAFT",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.DRAFT"
}, {
"rawName" : "DocumentState.APPROVED",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.APPROVED"
} ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "DocumentState.DRAFT",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.DRAFT"
} ],
"targetStates" : [ {
"rawName" : "DocumentState.IN_REVIEW",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW"
} ],
"event" : {
"rawName" : "DocumentEvent.SUBMIT",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "DocumentState.IN_REVIEW",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW"
} ],
"targetStates" : [ {
"rawName" : "DocumentState.APPROVED",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.APPROVED"
} ],
"event" : {
"rawName" : "DocumentEvent.APPROVE",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "DocumentState.IN_REVIEW",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW"
} ],
"targetStates" : [ {
"rawName" : "DocumentState.DRAFT",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentState.DRAFT"
} ],
"event" : {
"rawName" : "DocumentEvent.REJECT",
"fullIdentifier" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
} ],
"startStates" : [ "click.kamil.enterprise.machines.document.DocumentState.DRAFT" ],
"endStates" : [ "click.kamil.enterprise.machines.document.DocumentState.APPROVED" ],
"renderChoicesAsDiamonds" : true,
"flows" : [ ],
"stateTypeFqn" : "click.kamil.enterprise.machines.document.DocumentState",
"eventTypeFqn" : "click.kamil.enterprise.machines.document.DocumentEvent",
"metadata" : {
"triggers" : [ {
"event" : "OrderEvent.PAY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 30,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "OrderEvent.SHIP",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 37,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "DocumentEvent.SUBMIT",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "submitDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -40,7 +83,7 @@
"constraint" : null,
"ambiguous" : false
}, {
"event" : "DocumentEvent.APPROVE",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "approveDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -53,7 +96,7 @@
"constraint" : null,
"ambiguous" : false
}, {
"event" : "DocumentEvent.REJECT",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "rejectDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -65,32 +108,6 @@
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "UserEvent.VERIFY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "verifyUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 65,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "UserEvent.SUSPEND",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "suspendUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 72,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "event",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
@@ -98,145 +115,14 @@
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "ORDER",
"lineNumber" : 81,
"polymorphicEvents" : null,
"external" : false,
"constraint" : "\"ORDER\".equalsIgnoreCase(machineType)",
"ambiguous" : false
}, {
"event" : "event",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "DOCUMENT",
"sourceState" : null,
"lineNumber" : 87,
"polymorphicEvents" : null,
"external" : false,
"constraint" : "!(\"ORDER\".equalsIgnoreCase(machineType)) && \"DOCUMENT\".equalsIgnoreCase(machineType)",
"ambiguous" : false
}, {
"event" : "event",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "USER",
"lineNumber" : 93,
"polymorphicEvents" : null,
"external" : false,
"constraint" : "!(\"ORDER\".equalsIgnoreCase(machineType)) && !(\"DOCUMENT\".equalsIgnoreCase(machineType)) && \"USER\".equalsIgnoreCase(machineType)",
"ambiguous" : false
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /api/machine/order/pay",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/pay",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/order/ship",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/ship",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/document/submit",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "submitDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/document/approve",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "approveDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/approve",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/document/reject",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "rejectDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/reject",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/user/verify",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "verifyUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/user/verify",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/user/suspend",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "suspendUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/user/suspend",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/{machineType}/transition/{event}",
"className" : "click.kamil.enterprise.web.StateMachineController",
@@ -261,74 +147,6 @@
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/order/pay",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/pay",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.payOrder", "click.kamil.enterprise.web.StateMachineDispatcher.payOrder" ],
"triggerPoint" : {
"event" : "OrderEvent.PAY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 30,
"polymorphicEvents" : [ "OrderEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/order/ship",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/ship",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.shipOrder", "click.kamil.enterprise.web.StateMachineDispatcher.shipOrder" ],
"triggerPoint" : {
"event" : "OrderEvent.SHIP",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 37,
"polymorphicEvents" : [ "OrderEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/document/submit",
@@ -347,7 +165,7 @@
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.submitDocument", "click.kamil.enterprise.web.StateMachineDispatcher.submitDocument" ],
"triggerPoint" : {
"event" : "DocumentEvent.SUBMIT",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "submitDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -355,16 +173,16 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 44,
"polymorphicEvents" : [ "DocumentEvent.SUBMIT" ],
"polymorphicEvents" : [ "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.DRAFT",
"targetState" : "DocumentState.IN_REVIEW",
"event" : "DocumentEvent.SUBMIT"
"sourceState" : "click.kamil.enterprise.machines.document.DocumentState.DRAFT",
"targetState" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT"
} ]
}, {
"entryPoint" : {
@@ -385,7 +203,7 @@
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.approveDocument", "click.kamil.enterprise.web.StateMachineDispatcher.approveDocument" ],
"triggerPoint" : {
"event" : "DocumentEvent.APPROVE",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "approveDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -393,16 +211,16 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 51,
"polymorphicEvents" : [ "DocumentEvent.APPROVE" ],
"polymorphicEvents" : [ "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.IN_REVIEW",
"targetState" : "DocumentState.APPROVED",
"event" : "DocumentEvent.APPROVE"
"sourceState" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW",
"targetState" : "click.kamil.enterprise.machines.document.DocumentState.APPROVED",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE"
} ]
}, {
"entryPoint" : {
@@ -423,7 +241,7 @@
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.rejectDocument", "click.kamil.enterprise.web.StateMachineDispatcher.rejectDocument" ],
"triggerPoint" : {
"event" : "DocumentEvent.REJECT",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "rejectDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -431,85 +249,17 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 58,
"polymorphicEvents" : [ "DocumentEvent.REJECT" ],
"polymorphicEvents" : [ "click.kamil.enterprise.machines.document.DocumentEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "DocumentState.IN_REVIEW",
"targetState" : "DocumentState.DRAFT",
"event" : "DocumentEvent.REJECT"
"sourceState" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW",
"targetState" : "click.kamil.enterprise.machines.document.DocumentState.DRAFT",
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/user/verify",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "verifyUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/user/verify",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.verifyUser", "click.kamil.enterprise.web.StateMachineDispatcher.verifyUser" ],
"triggerPoint" : {
"event" : "UserEvent.VERIFY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "verifyUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 65,
"polymorphicEvents" : [ "UserEvent.VERIFY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/user/suspend",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "suspendUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/user/suspend",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.suspendUser", "click.kamil.enterprise.web.StateMachineDispatcher.suspendUser" ],
"triggerPoint" : {
"event" : "UserEvent.SUSPEND",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "suspendUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 72,
"polymorphicEvents" : [ "UserEvent.SUSPEND" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
@@ -543,49 +293,7 @@
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "ORDER",
"lineNumber" : 81,
"polymorphicEvents" : [ "<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>" ],
"external" : true,
"constraint" : "\"ORDER\".equalsIgnoreCase(machineType)",
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/{machineType}/transition/{event}",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "transition",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/{machineType}/transition/{event}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "machineType",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "event",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
"triggerPoint" : {
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "DOCUMENT",
"sourceState" : null,
"lineNumber" : 87,
"polymorphicEvents" : [ "<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>" ],
"external" : true,
@@ -594,107 +302,9 @@
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/{machineType}/transition/{event}",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "transition",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/{machineType}/transition/{event}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "machineType",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "event",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
"triggerPoint" : {
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "USER",
"lineNumber" : 93,
"polymorphicEvents" : [ "<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>" ],
"external" : true,
"constraint" : "!(\"ORDER\".equalsIgnoreCase(machineType)) && !(\"DOCUMENT\".equalsIgnoreCase(machineType)) && \"USER\".equalsIgnoreCase(machineType)",
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
} ],
"properties" : {
"default" : { }
}
},
"name" : "click.kamil.enterprise.machines.document.DocumentStateMachineConfiguration",
"renderChoicesAsDiamonds" : true,
"startStates" : [ "DocumentState.DRAFT" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "DocumentState.DRAFT",
"fullIdentifier" : "DocumentState.DRAFT"
} ],
"targetStates" : [ {
"rawName" : "DocumentState.IN_REVIEW",
"fullIdentifier" : "DocumentState.IN_REVIEW"
} ],
"event" : {
"rawName" : "DocumentEvent.SUBMIT",
"fullIdentifier" : "DocumentEvent.SUBMIT"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "DocumentState.IN_REVIEW",
"fullIdentifier" : "DocumentState.IN_REVIEW"
} ],
"targetStates" : [ {
"rawName" : "DocumentState.APPROVED",
"fullIdentifier" : "DocumentState.APPROVED"
} ],
"event" : {
"rawName" : "DocumentEvent.APPROVE",
"fullIdentifier" : "DocumentEvent.APPROVE"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "DocumentState.IN_REVIEW",
"fullIdentifier" : "DocumentState.IN_REVIEW"
} ],
"targetStates" : [ {
"rawName" : "DocumentState.DRAFT",
"fullIdentifier" : "DocumentState.DRAFT"
} ],
"event" : {
"rawName" : "DocumentEvent.REJECT",
"fullIdentifier" : "DocumentEvent.REJECT"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
} ],
"endStates" : [ "DocumentState.APPROVED" ]
}
}

View File

@@ -21,13 +21,13 @@ skinparam ArrowThickness 1
skinparam dpi 110
skinparam svgLinkTarget _self
[*] --> DocumentState.DRAFT
[*] --> click.kamil.enterprise.machines.document.DocumentState.DRAFT
DocumentState.DRAFT -[#1E90FF,bold]-> DocumentState.IN_REVIEW <<external>> : DocumentEvent.SUBMIT
DocumentState.IN_REVIEW -[#1E90FF,bold]-> DocumentState.APPROVED <<external>> : DocumentEvent.APPROVE
DocumentState.IN_REVIEW -[#1E90FF,bold]-> DocumentState.DRAFT <<external>> : DocumentEvent.REJECT
DocumentState.APPROVED --> [*]
click.kamil.enterprise.machines.document.DocumentState.APPROVED --> [*]
@enduml

View File

@@ -9,5 +9,9 @@
</state>
<state id="APPROVED">
</state>
<state id="DRAFT">
</state>
<state id="APPROVED">
</state>
</scxml>

View File

@@ -1,9 +1,6 @@
{
"name" : "click.kamil.examples.enterprise.config.EnterpriseStateMachineConfig",
"states" : [ {
"rawName" : "String.CANCELLED",
"fullIdentifier" : "String.CANCELLED"
}, {
"rawName" : "\"NEW\"",
"fullIdentifier" : "String.NEW"
}, {
@@ -18,24 +15,15 @@
}, {
"rawName" : "\"PAID\"",
"fullIdentifier" : "String.PAID"
}, {
"rawName" : "String.DELIVERED",
"fullIdentifier" : "String.DELIVERED"
}, {
"rawName" : "\"CHECK_AVAILABILITY\"",
"fullIdentifier" : "String.CHECK_AVAILABILITY"
}, {
"rawName" : "\"DELIVERED\"",
"fullIdentifier" : "String.DELIVERED"
}, {
"rawName" : "String.NEW",
"fullIdentifier" : "String.NEW"
}, {
"rawName" : "\"RETURNED\"",
"fullIdentifier" : "String.RETURNED"
}, {
"rawName" : "String.RETURNED",
"fullIdentifier" : "String.RETURNED"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -12,9 +12,6 @@
}, {
"rawName" : "\"START\"",
"fullIdentifier" : "String.START"
}, {
"rawName" : "String.START",
"fullIdentifier" : "String.START"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -12,9 +12,6 @@
}, {
"rawName" : "\"START\"",
"fullIdentifier" : "String.START"
}, {
"rawName" : "String.START",
"fullIdentifier" : "String.START"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -21,9 +21,6 @@
}, {
"rawName" : "States.STATE_EXTRA_2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE_EXTRA_2"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1"
}, {
"rawName" : "States.STATE11",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE11"
@@ -66,9 +63,6 @@
}, {
"rawName" : "States.STATE_EXTRA_3",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE_EXTRA_3"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ"
}, {
"rawName" : "States.CANCEL",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.CANCEL"

View File

@@ -24,9 +24,6 @@
}, {
"rawName" : "States.STATEY",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEY"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1"
}, {
"rawName" : "States.STATE11",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE11"
@@ -69,9 +66,6 @@
}, {
"rawName" : "States.STATE_EXTRA_1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE_EXTRA_1"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ"
}, {
"rawName" : "States.CANCEL",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.CANCEL"

View File

@@ -21,15 +21,9 @@
}, {
"rawName" : "States.END",
"fullIdentifier" : "ForkJoinStateMachineConfig.States.END"
}, {
"rawName" : "ForkJoinStateMachineConfig.States.END",
"fullIdentifier" : "ForkJoinStateMachineConfig.States.END"
}, {
"rawName" : "States.FORK",
"fullIdentifier" : "ForkJoinStateMachineConfig.States.FORK"
}, {
"rawName" : "ForkJoinStateMachineConfig.States.START",
"fullIdentifier" : "ForkJoinStateMachineConfig.States.START"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -21,9 +21,6 @@
}, {
"rawName" : "States.STATE_EXTRA_2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE_EXTRA_2"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1"
}, {
"rawName" : "States.STATE2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE2"
@@ -45,9 +42,6 @@
}, {
"rawName" : "States.STATE_EXTRA_3",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE_EXTRA_3"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ"
}, {
"rawName" : "States.CANCEL",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.CANCEL"

View File

@@ -21,9 +21,6 @@
}, {
"rawName" : "States.STATE_EXTRA_2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE_EXTRA_2"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE1"
}, {
"rawName" : "States.STATE2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE2"
@@ -45,9 +42,6 @@
}, {
"rawName" : "States.STATE_EXTRA_1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATE_EXTRA_1"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.STATEZ"
}, {
"rawName" : "States.CANCEL",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate3.States.CANCEL"

View File

@@ -6,9 +6,6 @@
}, {
"rawName" : "\"START\"",
"fullIdentifier" : "String.START"
}, {
"rawName" : "String.START",
"fullIdentifier" : "String.START"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -9,9 +9,6 @@
}, {
"rawName" : "OrderStates.FULFILLED",
"fullIdentifier" : "click.kamil.examples.statemachine.enumstate.OrderStates.FULFILLED"
}, {
"rawName" : "click.kamil.examples.statemachine.enumstate.OrderStates.CANCELED",
"fullIdentifier" : "click.kamil.examples.statemachine.enumstate.OrderStates.CANCELED"
}, {
"rawName" : "OrderStates.PAID1",
"fullIdentifier" : "click.kamil.examples.statemachine.enumstate.OrderStates.PAID1"
@@ -24,12 +21,6 @@
}, {
"rawName" : "OrderStates.PAID",
"fullIdentifier" : "click.kamil.examples.statemachine.enumstate.OrderStates.PAID"
}, {
"rawName" : "click.kamil.examples.statemachine.enumstate.OrderStates.SUBMITTED",
"fullIdentifier" : "click.kamil.examples.statemachine.enumstate.OrderStates.SUBMITTED"
}, {
"rawName" : "click.kamil.examples.statemachine.enumstate.OrderStates.FULFILLED",
"fullIdentifier" : "click.kamil.examples.statemachine.enumstate.OrderStates.FULFILLED"
}, {
"rawName" : "OrderStates.HAPPEN",
"fullIdentifier" : "click.kamil.examples.statemachine.enumstate.OrderStates.HAPPEN"

View File

@@ -3,18 +3,12 @@
"states" : [ {
"rawName" : "\"DONE\"",
"fullIdentifier" : "String.DONE"
}, {
"rawName" : "String.INIT",
"fullIdentifier" : "String.INIT"
}, {
"rawName" : "\"INIT\"",
"fullIdentifier" : "String.INIT"
}, {
"rawName" : "\"BUSY\"",
"fullIdentifier" : "String.BUSY"
}, {
"rawName" : "String.DONE",
"fullIdentifier" : "String.DONE"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -1,679 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>State Machine Explorer - click.kamil.maven.core.MavenOrderStateMachine</title>
<!-- Popper and Tippy for tooltips -->
<script src="https://unpkg.com/@popperjs/core@2"></script>
<script src="https://unpkg.com/tippy.js@6"></script>
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/scale.css"/>
<!-- SVG Pan & Zoom -->
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-bg: #ffffff;
--main-bg: #f8fafc;
--accent: #ef4444;
--primary: #3b82f6;
--text: #0f172a;
--text-muted: #64748b;
--border: #e2e8f0;
}
body {
margin: 0;
display: flex;
height: 100vh;
font-family: 'Inter', -apple-system, sans-serif;
background: var(--main-bg);
color: var(--text);
overflow: hidden;
}
#sidebar {
width: 480px;
min-width: 350px;
max-width: 900px;
background: var(--sidebar-bg);
border-right: 2px solid var(--border);
display: flex;
flex-direction: column;
box-shadow: 10px 0 15px rgba(0,0,0,0.02);
z-index: 10;
overflow-x: hidden;
resize: horizontal;
position: relative;
transition: width 0.3s ease, min-width 0.3s ease, padding 0.3s ease;
}
#sidebar.collapsed {
width: 0 !important;
min-width: 0 !important;
border-right: none;
padding: 0;
overflow: hidden;
resize: none;
}
#toggle-sidebar {
position: absolute;
top: 20px;
left: 20px;
z-index: 50;
background: #fff;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px;
cursor: pointer;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
#toggle-sidebar:hover {
background: #f8fafc;
border-color: var(--primary);
}
#toggle-sidebar svg {
width: 20px;
height: 20px;
fill: none;
stroke: var(--text-muted);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
#sidebar::after {
content: '⋮';
position: absolute;
right: 2px; top: 50%;
transform: translateY(-50%);
color: var(--border);
font-size: 1.2rem;
pointer-events: none;
}
.sidebar-header {
padding: 30px 30px 10px;
}
header h1 { font-weight: 900; font-size: 1.6rem; color: var(--text); margin: 0 0 5px 0; letter-spacing: -0.5px; }
header p { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); margin: 0 0 15px 0; opacity: 0.7; }
.tabs {
display: flex;
padding: 0 30px;
border-bottom: 1px solid var(--border);
gap: 20px;
}
.tab {
padding: 10px 0;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
cursor: pointer;
position: relative;
transition: color 0.2s;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--primary); }
.tab.active::after {
content: '';
position: absolute;
bottom: -1px; left: 0; right: 0;
height: 2px;
background: var(--primary);
}
.tab-content {
flex-grow: 1;
padding: 25px 30px;
overflow-y: auto;
display: none;
}
.tab-content.active { display: block; }
.ep-card, .flow-card {
padding: 18px;
background: #fff;
border: 1px solid var(--border);
border-radius: 14px;
margin-bottom: 15px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
position: relative;
word-break: break-all;
overflow-wrap: anywhere;
}
.ep-card:hover, .flow-card:hover {
border-color: var(--primary);
transform: translateX(5px);
box-shadow: 0 15px 30px -10px rgba(59, 130, 246, 0.15);
}
.ep-type {
font-size: 0.6rem;
font-weight: 900;
padding: 3px 8px;
border-radius: 6px;
display: inline-block;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.type-rest { background: #eff6ff; color: #1d4ed8; }
.type-custom { background: #fef2f2; color: #b91c1c; }
.type-jms { background: #f5f3ff; color: #7c3aed; }
.type-sqs { background: #fff7ed; color: #ea580c; }
.type-sns { background: #fff1f2; color: #e11d48; }
.type-kafka { background: #fefce8; color: #854d0e; }
.type-rabbit { background: #f0fdf4; color: #166534; }
.ep-name, .flow-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); }
.ep-fqn, .flow-desc { font-family: 'Inter', sans-serif; font-size: 0.75rem; color: var(--text-muted); opacity: 0.9; line-height: 1.4; }
#main { flex-grow: 1; position: relative; background: var(--main-bg); overflow: hidden; }
#svg-container {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
svg {
width: 100%;
height: 100%;
display: block;
}
/* SVG Interactivity Styles */
svg a { text-decoration: none !important; }
.active-path {
stroke: var(--accent) !important;
stroke-width: 2.5px !important;
filter: drop-shadow(0 0 8px rgba(239, 68, 68, 0.3));
transition: all 0.3s;
}
.active-path text, a.active-path text {
fill: #000 !important;
font-weight: 900 !important;
paint-order: stroke;
stroke: #fff;
stroke-width: 3.5px;
}
.dimmed {
opacity: 0.2 !important;
filter: grayscale(1);
transition: opacity 0.4s ease;
pointer-events: none;
}
/* Tippy Styling */
.tippy-box[data-theme~='modern'] {
background-color: #fff;
color: var(--text);
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
border: 1px solid var(--border);
border-radius: 16px;
padding: 0;
max-width: 550px !important;
}
.tooltip-content {
padding: 20px;
max-width: 500px;
word-break: break-word;
overflow-wrap: anywhere;
}
.payload-box {
background: #0f172a;
color: #e2e8f0;
border-radius: 8px;
padding: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
line-height: 1.5;
margin-top: 10px;
}
.payload-param {
margin-bottom: 4px;
}
.payload-param:last-child { margin-bottom: 0; }
</style>
</head>
<body>
<div id="sidebar">
<div class="sidebar-header">
<header>
<h1>Explorer</h1>
<p>click.kamil.maven.core.MavenOrderStateMachine</p>
</header>
</div>
<div class="tabs">
<div class="tab active" data-tab="explorer">Explorer</div>
<div class="tab" data-tab="flows">Business Flows</div>
</div>
<div id="tab-explorer" class="tab-content active">
<div id="ep-list"></div>
</div>
<div id="tab-flows" class="tab-content">
<div id="flow-list"></div>
</div>
</div>
<div id="main">
<button id="toggle-sidebar" aria-label="Toggle Sidebar" title="Toggle Sidebar">
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button>
<div id="svg-container">
<?xml version="1.0" encoding="us-ascii" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="100%" preserveAspectRatio="xMidYMid meet" version="1.1" viewBox="0 0 187 255" width="100%" zoomAndPan="magnify"><defs/><g><ellipse cx="35.5208" cy="34.375" fill="#222222" rx="11.4583" ry="11.4583" style="stroke:#222222;stroke-width:1.1458333333333333;"/><rect fill="#FFFFFF" height="45.8333" rx="14.3229" ry="14.3229" style="stroke:#94A3B8;stroke-width:1.1458333333333333;" width="57.2917" x="6.875" y="101.9792"/><text fill="#000000" font-family="Inter" font-size="10.3125" font-weight="bold" lengthAdjust="spacing" textLength="21.7708" x="24.6354" y="128.7932">INIT</text><rect fill="#FFFFFF" height="45.8333" rx="14.3229" ry="14.3229" style="stroke:#94A3B8;stroke-width:1.1458333333333333;" width="57.2917" x="6.875" y="202.8125"/><text fill="#000000" font-family="Inter" font-size="10.3125" font-weight="bold" lengthAdjust="spacing" textLength="27.5" x="21.7708" y="229.6266">BUSY</text><rect fill="#FFFFFF" height="45.8333" rx="14.3229" ry="14.3229" style="stroke:#94A3B8;stroke-width:1.1458333333333333;" width="57.2917" x="68.75" y="13.75"/><text fill="#000000" font-family="Inter" font-size="10.3125" font-weight="bold" lengthAdjust="spacing" textLength="29.7917" x="82.5" y="40.5641">DONE</text><ellipse cx="97.3958" cy="123.75" fill="none" rx="12.6042" ry="12.6042" style="stroke:#222222;stroke-width:1.1458333333333333;"/><ellipse cx="97.3958" cy="123.75" fill="#222222" rx="6.875" ry="6.875" style="stroke:#222222;stroke-width:1.1458333333333333;"/><polygon fill="#CBD5E1" points="35.5208,101.6748,40.1042,91.3623,35.5208,95.9456,30.9375,91.3623,35.5208,101.6748" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><path d="M35.5208,50.4929 C35.5208,63.9394 35.5208,78.5626 35.5208,94.7998 " fill="none" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><polygon fill="#1E90FF" points="35.5208,202.7749,40.1042,192.4624,35.5208,197.0457,30.9375,192.4624,35.5208,202.7749" style="stroke:#1E90FF;stroke-width:1.1458333333333333;"/><path d="M35.5208,147.9113 C35.5208,164.2413 35.5208,179.5846 35.5208,195.8999 " fill="none" style="stroke:#1E90FF;stroke-width:2.2916666666666665;"/><a href="#link_ORDER_EVENT" target="_self" title="#link_ORDER_EVENT" xlink:actuate="onRequest" xlink:href="#link_ORDER_EVENT" xlink:show="new" xlink:title="#link_ORDER_EVENT" xlink:type="simple"><text fill="#0000FF" font-family="JetBrains Mono" font-size="9.1667" lengthAdjust="spacing" text-decoration="underline" textLength="142.0833" x="36.6667" y="178.4456">ORDER_EVENT / loggingAction()</text></a><polygon fill="#CBD5E1" points="97.3958,110.7767,101.9792,100.4642,97.3958,105.0475,92.8125,100.4642,97.3958,110.7767" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><path d="M97.3958,59.9435 C97.3958,76.0566 97.3958,90.453 97.3958,103.9017 " fill="none" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><!--SRC=[RP5lJy8m4CRVzrESuOqQYSm_2HX20Z8IZ881D364a6CzjeQkNTeYJkDtjmF4XVYgrz-rzpntTv8PZ5C4YRbUEx0fELJ8BFcOCZJej06b5R54S09ACvS39niPaJcXrGvRHuQqopDYTYLKyI_r41t15mFeOBIAZLuhVg-bhxT9XAE2QyF9x5YbSOFNY_g1JX8HhHHP2u5dFQtS05E2ll9IUp0MdmIDtulB9S52I-x1QATb51cugddmZ9mB5VjQtsM72NAzAVWIfIrxRnkZDmVH1t8TWq9PUD9A__TiQwL-dDbt5YtuBGN7oNA3VocU2GY2MjdaU_mer6g29lPBcLkIIyQcvpEeLblG7_GdZB7YWEgq4eIDMgztKKnXvhETb_4RD9lquMUcKBPQnMK-77N3qJny3GSJJ-vWEgr8Br3cK8ulGUeuzbDgHyN6JyzcCyQwmq6uTU2T_000]--></g></svg>
</div>
</div>
<script id="metadata-json" type="application/json">
{
"name" : "click.kamil.maven.core.MavenOrderStateMachine",
"states" : [ {
"rawName" : "DONE",
"fullIdentifier" : "DONE"
}, {
"rawName" : "\"INIT\"",
"fullIdentifier" : "INIT"
}, {
"rawName" : "\"BUSY\"",
"fullIdentifier" : "BUSY"
}, {
"rawName" : "INIT",
"fullIdentifier" : "INIT"
} ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"INIT\"",
"fullIdentifier" : "INIT"
} ],
"targetStates" : [ {
"rawName" : "\"BUSY\"",
"fullIdentifier" : "BUSY"
} ],
"event" : "ORDER_EVENT",
"guard" : null,
"actions" : [ {
"expression" : "loggingAction()",
"isLambda" : false,
"internalLogic" : "context -> {\n System.out.println(\"LOG: State transition in progress...\");\n}\n",
"lineNumber" : 23
} ],
"order" : null
} ],
"startStates" : [ "INIT" ],
"endStates" : [ "DONE" ],
"renderChoicesAsDiamonds" : true,
"flows" : [ ],
"metadata" : {
"triggers" : [ {
"event" : "ORDER_EVENT",
"className" : "click.kamil.maven.core.OrderService",
"methodName" : "onMessage",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 34
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /reactive/order",
"className" : "click.kamil.maven.api.ReactiveOrderApi",
"methodName" : "orderRoutes",
"metadata" : {
"path" : "/reactive/order",
"verb" : "POST",
"style" : "WebFlux Functional"
},
"parameters" : [ ]
}, {
"type" : "JMS",
"name" : "JMS: order.queue",
"className" : "click.kamil.maven.api.JmsOrderListener",
"methodName" : "onMessage",
"metadata" : {
"protocol" : "JMS",
"destination" : "order.queue"
},
"parameters" : [ {
"name" : "message",
"type" : "String",
"annotations" : [ ]
} ]
} ],
"callChains" : [ ],
"properties" : {
"default" : { }
}
}
}
</script>
<script>
const metadata = JSON.parse(document.getElementById('metadata-json').textContent);
let panZoomInstance;
document.addEventListener('DOMContentLoaded', () => {
const svg = document.querySelector('#svg-container svg');
panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true });
document.getElementById('toggle-sidebar').addEventListener('click', () => {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('collapsed');
// Re-center SVG pan-zoom after transition
setTimeout(() => {
if (panZoomInstance) {
panZoomInstance.resize();
panZoomInstance.center();
}
}, 300);
});
setupTabs();
populateSidebar();
populateFlows();
attachInteractivity();
});
function setupTabs() {
const flows = metadata.flows || [];
if (flows.length === 0) {
document.querySelector('.tabs').style.display = 'none';
document.querySelector('.tab-content[data-tab="flows"]')?.remove();
return;
}
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab, .tab-content').forEach(el => el.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
});
});
}
function populateSidebar() {
const list = document.getElementById('ep-list');
const entryPoints = metadata.metadata.entryPoints || [];
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
entryPoints.forEach(ep => {
// Quality Filter: Hide if it doesn't trigger a machine event
const chains = (metadata.metadata.callChains || []).filter(c =>
c.entryPoint.className === ep.className &&
c.entryPoint.methodName === ep.methodName &&
machineEvents.has(c.triggerPoint.event)
);
if (chains.length === 0) return;
const card = document.createElement('div');
card.className = 'ep-card';
card.innerHTML = `
<div class="ep-type type-${ep.type.toLowerCase()}">${ep.type}</div>
<div class="ep-name">${ep.name}</div>
<div class="ep-fqn">${ep.className}.${ep.methodName}</div>
`;
card.onmouseenter = () => highlightEntryPoint(ep);
card.onmouseleave = clearHighlights;
list.appendChild(card);
});
}
function populateFlows() {
const list = document.getElementById('flow-list');
const flows = metadata.flows || [];
if (flows.length === 0) {
list.innerHTML = '<p style="color:var(--text-muted); font-size:0.8rem; font-style:italic;">No business flows defined.</p>';
return;
}
flows.forEach(flow => {
const card = document.createElement('div');
card.className = 'flow-card';
card.innerHTML = `
<div class="flow-name">${flow.name}</div>
<div class="flow-desc">${flow.description}</div>
`;
card.onmouseenter = () => highlightFlow(flow);
card.onmouseleave = clearHighlights;
list.appendChild(card);
});
}
function highlightEntryPoint(ep) {
clearHighlights();
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
const chains = metadata.metadata.callChains || [];
const relevantEvents = chains
.filter(c => c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName && machineEvents.has(c.triggerPoint.event))
.map(c => c.triggerPoint.event);
if (relevantEvents.length === 0) return;
highlightEvents(relevantEvents);
}
function highlightFlow(flow) {
clearHighlights();
if (!flow.steps || flow.steps.length === 0) return;
highlightEvents(flow.steps);
}
function highlightEvents(steps) {
const svg = document.querySelector('#svg-container svg');
// Dim everything first
svg.querySelectorAll('path, rect, circle, ellipse, text, polygon').forEach(el => {
el.classList.add('dimmed');
});
steps.forEach(step => {
let linkId = "";
if (step.includes("->")) {
// Named Choice Branch: "SRC->TGT"
const parts = step.split("->");
linkId = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
} else {
// Standard Event
linkId = "#link_" + normalize(step);
}
svg.querySelectorAll('a').forEach(link => {
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
if (href === linkId) {
highlightLinkGroup(link);
}
});
});
// Un-dim all state nodes for context
svg.querySelectorAll('rect, circle, ellipse, polygon').forEach(el => {
const isChoice = el.tagName === 'polygon' && el.points?.numberOfItems === 4;
if (isChoice || ['rect', 'circle', 'ellipse'].includes(el.tagName)) {
el.classList.remove('dimmed');
let next = el.nextElementSibling;
while(next && next.tagName === 'text') {
next.classList.remove('dimmed');
next = next.nextElementSibling;
}
}
});
}
function highlightLinkGroup(link) {
link.classList.add('active-path');
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
// Find path and polygon immediately before the <a> tag (interleaved)
let prev = link.previousElementSibling;
let foundPath = false;
let foundPolygon = false;
while (prev) {
if (prev.tagName === 'path' && !foundPath) {
prev.classList.remove('dimmed');
prev.classList.add('active-path');
foundPath = true;
} else if (prev.tagName === 'polygon' && !foundPolygon && prev.points?.numberOfItems !== 4) {
prev.classList.remove('dimmed');
prev.classList.add('active-path');
foundPolygon = true;
} else if (['rect', 'circle', 'ellipse'].includes(prev.tagName) ||
(prev.tagName === 'polygon' && prev.points?.numberOfItems === 4)) {
// Hit a state node, stop traversal
break;
}
if (foundPath && foundPolygon) break;
prev = prev.previousElementSibling;
}
}
function clearHighlights() {
document.querySelectorAll('.dimmed, .active-path').forEach(el => {
el.classList.remove('dimmed', 'active-path');
});
}
function attachInteractivity() {
const svg = document.querySelector('#svg-container svg');
const transitions = metadata.transitions || [];
svg.querySelectorAll('a').forEach(link => {
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
if (!href || !href.startsWith('#link_')) return;
const linkId = href.replace('#link_', '');
const isAnon = linkId.startsWith('anon_');
let metaTrans = null;
if (isAnon) {
metaTrans = transitions.find(t =>
!t.event &&
"anon_" + normalize(t.sourceStates[0].fullIdentifier) + "_" + normalize(t.targetStates[0].fullIdentifier) === linkId
);
} else {
metaTrans = transitions.find(t => normalize(t.event) === linkId);
}
if (metaTrans || isAnon) {
link.style.cursor = 'pointer';
const setupTippy = (el) => {
tippy(el, {
content: createTooltip(isAnon ? "Internal logic" : linkId, metaTrans),
allowHTML: true,
theme: 'modern',
interactive: true,
appendTo: document.body,
placement: 'right',
offset: [0, 20]
});
};
setupTippy(link);
let path = link.previousElementSibling;
while(path && path.tagName !== 'path') path = path.previousElementSibling;
if (path) {
path.style.cursor = 'pointer';
setupTippy(path);
}
}
});
}
function normalize(s) {
if (!s) return "";
return s.replace(/[^a-zA-Z0-9]/g, '_');
}
function createTooltip(name, trans) {
const chains = (metadata.metadata.callChains || []).filter(c => normalize(c.triggerPoint.event) === name);
let html = `<div class="tooltip-content">
<div style="font-weight:900; font-size:1.1rem; margin-bottom:12px; border-bottom:1px solid #eee; padding-bottom:8px; line-height:1.3;">
${name === 'Internal logic' ? name : 'Event: <span style="color:var(--accent)">' + name + '</span>'}
</div>`;
if (trans && trans.guard) {
const lineInfo = trans.guard.lineNumber ? ` <span style="color:#94a3b8; font-weight:400; font-size:0.6rem;">(Line: ${trans.guard.lineNumber})</span>` : '';
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Guard${lineInfo}</div>
<div style="background:#f1f5f9; padding:6px; border-radius:4px; font-family:monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${trans.guard.expression}</div>`;
if (trans.guard.internalLogic) {
html += `<div style="font-size:0.65rem; font-weight:800; color:#cbd5e1; text-transform:uppercase; margin-bottom:4px">Internal Logic</div>
<pre style="background:#1e293b; color:#f8fafc; padding:8px; border-radius:4px; font-family:'JetBrains Mono', monospace; font-size:0.7rem; overflow-x:auto; margin-bottom:12px;"><code>${trans.guard.internalLogic.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>`;
}
}
if (trans && trans.actions && trans.actions.length > 0) {
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Actions</div>`;
trans.actions.forEach(action => {
const lineInfo = action.lineNumber ? ` <span style="color:#94a3b8; font-weight:400; font-size:0.6rem;">(Line: ${action.lineNumber})</span>` : '';
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Action${lineInfo}</div>
<div style="background:#f1f5f9; padding:6px; border-radius:4px; font-family:monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${action.expression}</div>`;
if (action.internalLogic) {
html += `<div style="font-size:0.65rem; font-weight:800; color:#cbd5e1; text-transform:uppercase; margin-bottom:4px">Internal Logic</div>
<pre style="background:#1e293b; color:#f8fafc; padding:8px; border-radius:4px; font-family:'JetBrains Mono', monospace; font-size:0.7rem; overflow-x:auto; margin-bottom:12px;"><code>${action.internalLogic.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>`;
}
});
}
if (chains && chains.length > 0) {
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:10px; border-top:1px solid #eee; padding-top:10px">Triggered by</div>`;
chains.forEach(c => {
html += `<div style="margin-bottom:20px">
<div style="font-weight:700; font-size:0.85rem; color:var(--text);">${c.entryPoint.name}</div>
${renderParameters(c.entryPoint.parameters)}
<div style="margin-left:10px; border-left:2px solid #e2e8f0; padding-left:12px; margin-top:10px">
${c.methodChain.map((m, i) => `<div style="font-size:0.7rem; color:#475569; margin-top:4px; font-family:'JetBrains Mono', monospace;"><b>${i+1}</b> ${m}</div>`).join('')}
</div>
</div>`;
});
}
html += `</div>`;
return html;
}
function renderParameters(params) {
if (!params || params.length === 0) return "";
let html = `<div style="font-size:0.65rem; font-weight:700; color:#94a3b8; margin-top:8px; margin-bottom:4px; text-transform:uppercase;">Input Data</div>`;
html += `<div class="payload-box">`;
params.forEach(p => {
const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam"));
const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : "";
html += `<div class="payload-param">
<span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span>
<span style="color:#38bdf8;">${p.type}</span>
<span style="color:#a3e635;">${p.name}</span>
</div>`;
});
html += `</div>`;
return html;
}
</script>
</body>
</html>

View File

@@ -1,66 +0,0 @@
{
"metadata" : {
"triggers" : [ {
"event" : "ORDER_EVENT",
"className" : "click.kamil.maven.core.OrderService",
"methodName" : "onMessage",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 34
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /reactive/order",
"className" : "click.kamil.maven.api.ReactiveOrderApi",
"methodName" : "orderRoutes",
"metadata" : {
"path" : "/reactive/order",
"verb" : "POST",
"style" : "WebFlux Functional"
},
"parameters" : [ ]
}, {
"type" : "JMS",
"name" : "JMS: order.queue",
"className" : "click.kamil.maven.api.JmsOrderListener",
"methodName" : "onMessage",
"metadata" : {
"protocol" : "JMS",
"destination" : "order.queue"
},
"parameters" : [ {
"name" : "message",
"type" : "String",
"annotations" : [ ]
} ]
} ],
"callChains" : [ ],
"properties" : {
"default" : { }
}
},
"name" : "click.kamil.maven.core.MavenOrderStateMachine",
"renderChoicesAsDiamonds" : true,
"startStates" : [ "INIT" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"INIT\"",
"fullIdentifier" : "INIT"
} ],
"targetStates" : [ {
"rawName" : "\"BUSY\"",
"fullIdentifier" : "BUSY"
} ],
"event" : "ORDER_EVENT",
"guard" : null,
"actions" : [ {
"expression" : "loggingAction()",
"isLambda" : false,
"internalLogic" : "context -> {\n System.out.println(\"LOG: State transition in progress...\");\n}\n",
"lineNumber" : 23
} ],
"order" : null
} ],
"endStates" : [ "DONE" ]
}

View File

@@ -1,31 +0,0 @@
@startuml
!pragma layout smetana
set separator none
hide empty description
hide stereotype
skinparam state {
BackgroundColor white
BorderColor #94a3b8
BorderThickness 1
FontName Inter
FontSize 9
FontStyle bold
RoundCorner 20
Padding 1
}
skinparam shadowing false
skinparam ArrowFontName JetBrains Mono
skinparam ArrowFontSize 8
skinparam ArrowColor #cbd5e1
skinparam ArrowThickness 1
skinparam dpi 110
skinparam svgLinkTarget _self
[*] --> INIT
INIT -[#1E90FF,bold]-> BUSY <<external>> <<e_ORDER_EVENT>> : ORDER_EVENT / loggingAction()
DONE --> [*]
@enduml

View File

@@ -18,9 +18,6 @@
}, {
"rawName" : "States.STATE20",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritancestate.States.STATE20"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritancestate.States.STATEZ",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritancestate.States.STATEZ"
}, {
"rawName" : "States.STATE2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritancestate.States.STATE2"
@@ -54,9 +51,6 @@
}, {
"rawName" : "States.STATE18",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritancestate.States.STATE18"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritancestate.States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritancestate.States.STATE1"
}, {
"rawName" : "States.STATE_EXTRA_1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritancestate.States.STATE_EXTRA_1"

View File

@@ -9,12 +9,6 @@
}, {
"rawName" : "\"PROCESSING\"",
"fullIdentifier" : "String.PROCESSING"
}, {
"rawName" : "String.COMPLETED",
"fullIdentifier" : "String.COMPLETED"
}, {
"rawName" : "String.NEW",
"fullIdentifier" : "String.NEW"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -1,108 +0,0 @@
{
"metadata" : {
"triggers" : [ {
"event" : "SUBMIT",
"className" : "click.kamil.multi.core.OrderController",
"methodName" : "submitOrder",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 18
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /orders/submit",
"className" : "click.kamil.multi.core.OrderController",
"methodName" : "submitOrder",
"metadata" : {
"path" : "/orders/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
}, {
"type" : "REST",
"name" : "POST /orders/submit",
"className" : "click.kamil.multi.api.OrderApi",
"methodName" : "submitOrder",
"metadata" : {
"path" : "/orders/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /orders/submit",
"className" : "click.kamil.multi.core.OrderController",
"methodName" : "submitOrder",
"metadata" : {
"path" : "/orders/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "orderId",
"type" : "String",
"annotations" : [ "RequestBody" ]
} ]
},
"methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ],
"triggerPoint" : {
"event" : "SUBMIT",
"className" : "click.kamil.multi.core.OrderController",
"methodName" : "submitOrder",
"sourceModule" : null,
"stateMachineId" : null,
"lineNumber" : 18
}
} ],
"properties" : {
"default" : { }
}
},
"name" : "click.kamil.multi.core.OrderStateMachineConfig",
"renderChoicesAsDiamonds" : true,
"startStates" : [ "NEW" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"NEW\"",
"fullIdentifier" : "NEW"
} ],
"targetStates" : [ {
"rawName" : "\"PROCESSING\"",
"fullIdentifier" : "PROCESSING"
} ],
"event" : "SUBMIT",
"guard" : null,
"actions" : [ {
"expression" : "processAction()",
"isLambda" : false,
"internalLogic" : "@Override default void execute(StateContext<String,String> context){\n System.out.println(\"Processing order: \" + context.getMessageHeader(\"orderId\"));\n}\n",
"lineNumber" : 31
} ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "\"PROCESSING\"",
"fullIdentifier" : "PROCESSING"
} ],
"targetStates" : [ {
"rawName" : "\"COMPLETED\"",
"fullIdentifier" : "COMPLETED"
} ],
"event" : "FINISH",
"guard" : null,
"actions" : [ ],
"order" : null
} ],
"endStates" : [ "COMPLETED" ]
}

View File

@@ -9,12 +9,6 @@
}, {
"rawName" : "OrderState.SHIPPED",
"fullIdentifier" : "click.kamil.enterprise.machines.order.OrderState.SHIPPED"
}, {
"rawName" : "click.kamil.enterprise.machines.order.OrderState.SHIPPED",
"fullIdentifier" : "click.kamil.enterprise.machines.order.OrderState.SHIPPED"
}, {
"rawName" : "click.kamil.enterprise.machines.order.OrderState.NEW",
"fullIdentifier" : "click.kamil.enterprise.machines.order.OrderState.NEW"
} ],
"transitions" : [ {
"type" : "EXTERNAL",
@@ -98,7 +92,7 @@
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "click.kamil.enterprise.machines.order.OrderState.ORDER",
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : null,
"external" : false,
@@ -238,7 +232,7 @@
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "click.kamil.enterprise.machines.order.OrderState.ORDER",
"sourceState" : null,
"lineNumber" : 81,
"polymorphicEvents" : [ "<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>" ],
"external" : true,

View File

@@ -1,9 +1,6 @@
{
"name" : "click.kamil.examples.statemachine.polymorphic.PolymorphicStateMachineConfiguration",
"states" : [ {
"rawName" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED",
"fullIdentifier" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED"
}, {
"rawName" : "OrderStates.CANCELED",
"fullIdentifier" : "click.kamil.examples.statemachine.polymorphic.OrderStates.CANCELED"
}, {

View File

@@ -12,15 +12,6 @@
}, {
"rawName" : "OrderStates.PAID1",
"fullIdentifier" : "click.kamil.examples.statemachine.simple.OrderStates.PAID1"
}, {
"rawName" : "click.kamil.examples.statemachine.simple.OrderStates.SUBMITTED",
"fullIdentifier" : "click.kamil.examples.statemachine.simple.OrderStates.SUBMITTED"
}, {
"rawName" : "click.kamil.examples.statemachine.simple.OrderStates.FULFILLED",
"fullIdentifier" : "click.kamil.examples.statemachine.simple.OrderStates.FULFILLED"
}, {
"rawName" : "click.kamil.examples.statemachine.simple.OrderStates.CANCELED",
"fullIdentifier" : "click.kamil.examples.statemachine.simple.OrderStates.CANCELED"
}, {
"rawName" : "OrderStates.SUBMITTED",
"fullIdentifier" : "click.kamil.examples.statemachine.simple.OrderStates.SUBMITTED"

View File

@@ -3,9 +3,6 @@
"states" : [ {
"rawName" : "DocumentState.REJECTED",
"fullIdentifier" : "click.kamil.examples.statemachine.layered.document.DocumentState.REJECTED"
}, {
"rawName" : "click.kamil.examples.statemachine.layered.document.DocumentState.DRAFT",
"fullIdentifier" : "click.kamil.examples.statemachine.layered.document.DocumentState.DRAFT"
}, {
"rawName" : "DocumentState.DRAFT",
"fullIdentifier" : "click.kamil.examples.statemachine.layered.document.DocumentState.DRAFT"

View File

@@ -12,9 +12,6 @@
}, {
"rawName" : "OrderState.NEW",
"fullIdentifier" : "click.kamil.examples.statemachine.layered.order.OrderState.NEW"
}, {
"rawName" : "click.kamil.examples.statemachine.layered.order.OrderState.NEW",
"fullIdentifier" : "click.kamil.examples.statemachine.layered.order.OrderState.NEW"
} ],
"transitions" : [ {
"type" : "EXTERNAL",

View File

@@ -3,9 +3,6 @@
"states" : [ {
"rawName" : "OrderState.NEW",
"fullIdentifier" : "click.kamil.domain.OrderState.NEW"
}, {
"rawName" : "click.kamil.domain.OrderState.NEW",
"fullIdentifier" : "click.kamil.domain.OrderState.NEW"
}, {
"rawName" : "OrderState.CANCELLED",
"fullIdentifier" : "click.kamil.domain.OrderState.CANCELLED"

View File

@@ -21,9 +21,6 @@
}, {
"rawName" : "States.STATE_EXTRA_3",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATE_EXTRA_3"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATE1"
}, {
"rawName" : "States.STATE2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATE2"
@@ -66,9 +63,6 @@
}, {
"rawName" : "States.STATE_EXTRA_2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATE_EXTRA_2"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATEZ",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATEZ"
}, {
"rawName" : "States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate2.States.STATE1"

View File

@@ -1,9 +1,6 @@
{
"name" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.TwoStateMachineConfiguration",
"states" : [ {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.States.STATE1"
}, {
"rawName" : "States.STATE1",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.States.STATE1"
}, {
@@ -39,9 +36,6 @@
}, {
"rawName" : "States.STATE20",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.States.STATE20"
}, {
"rawName" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.States.STATEZ",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.States.STATEZ"
}, {
"rawName" : "States.STATE2",
"fullIdentifier" : "click.kamil.examples.statemachine.inheritanceextrafunctionsstate.States.STATE2"

View File

@@ -1,72 +1,59 @@
{
"name" : "click.kamil.enterprise.machines.user.UserStateMachineConfiguration",
"states" : [ {
"rawName" : "UserState.GUEST",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserState.GUEST"
}, {
"rawName" : "UserState.REGISTERED",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserState.REGISTERED"
}, {
"rawName" : "UserState.VERIFIED",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserState.VERIFIED"
} ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "UserState.GUEST",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserState.GUEST"
} ],
"targetStates" : [ {
"rawName" : "UserState.REGISTERED",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserState.REGISTERED"
} ],
"event" : {
"rawName" : "UserEvent.REGISTER",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserEvent.REGISTER"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "UserState.REGISTERED",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserState.REGISTERED"
} ],
"targetStates" : [ {
"rawName" : "UserState.VERIFIED",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserState.VERIFIED"
} ],
"event" : {
"rawName" : "UserEvent.VERIFY",
"fullIdentifier" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
} ],
"startStates" : [ "click.kamil.enterprise.machines.user.UserState.GUEST" ],
"endStates" : [ "click.kamil.enterprise.machines.user.UserState.VERIFIED" ],
"renderChoicesAsDiamonds" : true,
"flows" : [ ],
"stateTypeFqn" : "click.kamil.enterprise.machines.user.UserState",
"eventTypeFqn" : "click.kamil.enterprise.machines.user.UserEvent",
"metadata" : {
"triggers" : [ {
"event" : "OrderEvent.PAY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 30,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "OrderEvent.SHIP",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 37,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "DocumentEvent.SUBMIT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "submitDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 44,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "DocumentEvent.APPROVE",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "approveDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 51,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "DocumentEvent.REJECT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "rejectDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 58,
"polymorphicEvents" : null,
"external" : false,
"constraint" : null,
"ambiguous" : false
}, {
"event" : "UserEvent.VERIFY",
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "verifyUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -79,7 +66,7 @@
"constraint" : null,
"ambiguous" : false
}, {
"event" : "UserEvent.SUSPEND",
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "suspendUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -98,33 +85,7 @@
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "ORDER",
"lineNumber" : 81,
"polymorphicEvents" : null,
"external" : false,
"constraint" : "\"ORDER\".equalsIgnoreCase(machineType)",
"ambiguous" : false
}, {
"event" : "event",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "DOCUMENT",
"lineNumber" : 87,
"polymorphicEvents" : null,
"external" : false,
"constraint" : "!(\"ORDER\".equalsIgnoreCase(machineType)) && \"DOCUMENT\".equalsIgnoreCase(machineType)",
"ambiguous" : false
}, {
"event" : "event",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "USER",
"sourceState" : null,
"lineNumber" : 93,
"polymorphicEvents" : null,
"external" : false,
@@ -132,111 +93,6 @@
"ambiguous" : false
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /api/machine/order/pay",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/pay",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/order/ship",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/ship",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/document/submit",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "submitDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/document/approve",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "approveDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/approve",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/document/reject",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "rejectDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/reject",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/user/verify",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "verifyUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/user/verify",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/user/suspend",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "suspendUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/user/suspend",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/machine/{machineType}/transition/{event}",
"className" : "click.kamil.enterprise.web.StateMachineController",
@@ -261,176 +117,6 @@
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/order/pay",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/pay",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.payOrder", "click.kamil.enterprise.web.StateMachineDispatcher.payOrder" ],
"triggerPoint" : {
"event" : "OrderEvent.PAY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "payOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 30,
"polymorphicEvents" : [ "OrderEvent.PAY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/order/ship",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/order/ship",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.shipOrder", "click.kamil.enterprise.web.StateMachineDispatcher.shipOrder" ],
"triggerPoint" : {
"event" : "OrderEvent.SHIP",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "shipOrder",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 37,
"polymorphicEvents" : [ "OrderEvent.SHIP" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/document/submit",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "submitDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/submit",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.submitDocument", "click.kamil.enterprise.web.StateMachineDispatcher.submitDocument" ],
"triggerPoint" : {
"event" : "DocumentEvent.SUBMIT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "submitDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 44,
"polymorphicEvents" : [ "DocumentEvent.SUBMIT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/document/approve",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "approveDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/approve",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.approveDocument", "click.kamil.enterprise.web.StateMachineDispatcher.approveDocument" ],
"triggerPoint" : {
"event" : "DocumentEvent.APPROVE",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "approveDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 51,
"polymorphicEvents" : [ "DocumentEvent.APPROVE" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/document/reject",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "rejectDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/document/reject",
"verb" : "POST"
},
"parameters" : [ {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.rejectDocument", "click.kamil.enterprise.web.StateMachineDispatcher.rejectDocument" ],
"triggerPoint" : {
"event" : "DocumentEvent.REJECT",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "rejectDocument",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 58,
"polymorphicEvents" : [ "DocumentEvent.REJECT" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/user/verify",
@@ -449,7 +135,7 @@
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.verifyUser", "click.kamil.enterprise.web.StateMachineDispatcher.verifyUser" ],
"triggerPoint" : {
"event" : "UserEvent.VERIFY",
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "verifyUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -457,16 +143,16 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 65,
"polymorphicEvents" : [ "UserEvent.VERIFY" ],
"polymorphicEvents" : [ "click.kamil.enterprise.machines.user.UserEvent.VERIFY" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "UserState.REGISTERED",
"targetState" : "UserState.VERIFIED",
"event" : "UserEvent.VERIFY"
"sourceState" : "click.kamil.enterprise.machines.user.UserState.REGISTERED",
"targetState" : "click.kamil.enterprise.machines.user.UserState.VERIFIED",
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY"
} ]
}, {
"entryPoint" : {
@@ -487,7 +173,7 @@
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.suspendUser", "click.kamil.enterprise.web.StateMachineDispatcher.suspendUser" ],
"triggerPoint" : {
"event" : "UserEvent.SUSPEND",
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "suspendUser",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
@@ -495,7 +181,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 72,
"polymorphicEvents" : [ "UserEvent.SUSPEND" ],
"polymorphicEvents" : [ "click.kamil.enterprise.machines.user.UserEvent.SUSPEND" ],
"external" : false,
"constraint" : null,
"ambiguous" : false
@@ -535,91 +221,7 @@
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "ORDER",
"lineNumber" : 81,
"polymorphicEvents" : [ "<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>" ],
"external" : true,
"constraint" : "\"ORDER\".equalsIgnoreCase(machineType)",
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/{machineType}/transition/{event}",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "transition",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/{machineType}/transition/{event}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "machineType",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "event",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
"triggerPoint" : {
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "DOCUMENT",
"lineNumber" : 87,
"polymorphicEvents" : [ "<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>" ],
"external" : true,
"constraint" : "!(\"ORDER\".equalsIgnoreCase(machineType)) && \"DOCUMENT\".equalsIgnoreCase(machineType)",
"ambiguous" : false
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/machine/{machineType}/transition/{event}",
"className" : "click.kamil.enterprise.web.StateMachineController",
"methodName" : "transition",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
"metadata" : {
"path" : "/api/machine/{machineType}/transition/{event}",
"verb" : "POST"
},
"parameters" : [ {
"name" : "machineType",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "event",
"type" : "String",
"annotations" : [ "PathVariable" ]
}, {
"name" : "userId",
"type" : "String",
"annotations" : [ "RequestParam" ]
} ]
},
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
"triggerPoint" : {
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
"methodName" : "dispatch",
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : "USER",
"sourceState" : null,
"lineNumber" : 93,
"polymorphicEvents" : [ "<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>" ],
"external" : true,
@@ -632,44 +234,5 @@
"properties" : {
"default" : { }
}
},
"name" : "click.kamil.enterprise.machines.user.UserStateMachineConfiguration",
"renderChoicesAsDiamonds" : true,
"startStates" : [ "UserState.GUEST" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "UserState.GUEST",
"fullIdentifier" : "UserState.GUEST"
} ],
"targetStates" : [ {
"rawName" : "UserState.REGISTERED",
"fullIdentifier" : "UserState.REGISTERED"
} ],
"event" : {
"rawName" : "UserEvent.REGISTER",
"fullIdentifier" : "UserEvent.REGISTER"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "UserState.REGISTERED",
"fullIdentifier" : "UserState.REGISTERED"
} ],
"targetStates" : [ {
"rawName" : "UserState.VERIFIED",
"fullIdentifier" : "UserState.VERIFIED"
} ],
"event" : {
"rawName" : "UserEvent.VERIFY",
"fullIdentifier" : "UserEvent.VERIFY"
},
"guards" : [ ],
"actions" : [ ],
"order" : null
} ],
"endStates" : [ "UserState.VERIFIED" ]
}
}

View File

@@ -21,12 +21,12 @@ skinparam ArrowThickness 1
skinparam dpi 110
skinparam svgLinkTarget _self
[*] --> UserState.GUEST
[*] --> click.kamil.enterprise.machines.user.UserState.GUEST
UserState.GUEST -[#1E90FF,bold]-> UserState.REGISTERED <<external>> : UserEvent.REGISTER
UserState.REGISTERED -[#1E90FF,bold]-> UserState.VERIFIED <<external>> : UserEvent.VERIFY
UserState.VERIFIED --> [*]
click.kamil.enterprise.machines.user.UserState.VERIFIED --> [*]
@enduml

View File

@@ -8,5 +8,9 @@
</state>
<state id="VERIFIED">
</state>
<state id="GUEST">
</state>
<state id="VERIFIED">
</state>
</scxml>

View File

@@ -65,6 +65,9 @@ public class HtmlExporterCommand implements Callable<Integer> {
@Option(names = {"--include-generated-sources"}, description = "Scan existing Maven target/ and Gradle build/ trees for pre-generated .java sources. Does not run Maven or Gradle.", negatable = true, defaultValue = "true", fallbackValue = "true")
private boolean includeGeneratedSources = true;
@Option(names = {"--inline-accessors"}, description = "Index trivial JavaBean/record accessors at scan time and inline them during call-chain and constant resolution.", negatable = true, defaultValue = "true", fallbackValue = "true")
private boolean inlineAccessors = true;
@Override
public Integer call() throws Exception {
var out = spec.commandLine().getOut();
@@ -100,7 +103,7 @@ public class HtmlExporterCommand implements Callable<Integer> {
exportService.runJsonExporter(jsonFile, outputDir, formats, profiles, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, inputDir);
} else {
boolean renderChoicesAsDiamonds = !noDiamonds;
exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, flowsFile, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, includeGeneratedSources);
exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, flowsFile, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, includeGeneratedSources, inlineAccessors);
}
System.clearProperty("html.hideMetadataPane");

View File

@@ -356,19 +356,35 @@
});
}
function isLifecycleChain(c) {
const ev = c.triggerPoint && c.triggerPoint.event;
return ev && ev.startsWith('[LIFECYCLE:');
}
function formatLifecycleLabel(event) {
const m = event && event.match(/^\[LIFECYCLE:(\w+)\]$/);
if (!m) return event;
const type = m[1].toUpperCase();
if (type === 'RESTORE') return 'Restore persisted state';
if (type === 'RESET') return 'Reset state machine';
return 'Lifecycle: ' + m[1].toLowerCase();
}
function chainsForEntryPoint(ep) {
return (metadata.metadata.callChains || []).filter(c =>
c.entryPoint.className === ep.className &&
c.entryPoint.methodName === ep.methodName &&
((c.matchedTransitions && c.matchedTransitions.length > 0) || isLifecycleChain(c))
);
}
function populateSidebar() {
const list = document.getElementById('ep-list');
if (!list) return;
const entryPoints = metadata.metadata.entryPoints || [];
entryPoints.forEach(ep => {
// Quality Filter: Hide if it doesn't trigger a machine event
const chains = (metadata.metadata.callChains || []).filter(c =>
c.entryPoint.className === ep.className &&
c.entryPoint.methodName === ep.methodName &&
c.matchedTransitions && c.matchedTransitions.length > 0
);
const chains = chainsForEntryPoint(ep);
if (chains.length === 0) return;
const card = document.createElement('div');
@@ -381,18 +397,24 @@
${sourceHint}
`;
// Add tags for matched events
let triggersHtml = '<div class="ep-transitions" style="margin-top: 8px;">Triggers: ';
const addedEvents = new Set();
chains.forEach(c => {
c.matchedTransitions.forEach(t => {
const evName = t.event || "";
if (!addedEvents.has(evName)) {
addedEvents.add(evName);
triggersHtml += `<span class="badge transition" style="font-size: 0.65rem; padding: 2px 6px;">${evName}</span> `;
}
let triggersHtml = '<div class="ep-transitions" style="margin-top: 8px;">';
const lifecycleChain = chains.find(isLifecycleChain);
if (lifecycleChain) {
const label = formatLifecycleLabel(lifecycleChain.triggerPoint.event);
triggersHtml += `<span class="badge lifecycle" style="font-size: 0.65rem; padding: 2px 6px; background:#ecfdf5; color:#047857; border:1px solid #a7f3d0;">${label}</span>`;
} else {
triggersHtml += 'Triggers: ';
const addedEvents = new Set();
chains.forEach(c => {
c.matchedTransitions.forEach(t => {
const evName = t.event || "";
if (!addedEvents.has(evName)) {
addedEvents.add(evName);
triggersHtml += `<span class="badge transition" style="font-size: 0.65rem; padding: 2px 6px;">${evName}</span> `;
}
});
});
});
}
triggersHtml += '</div>';
card.innerHTML += triggersHtml;
@@ -427,20 +449,18 @@
function highlightEntryPoint(ep) {
clearHighlights();
const chains = metadata.metadata.callChains || [];
const chains = chainsForEntryPoint(ep);
const seen = new Set();
let steps = [];
chains.forEach(c => {
if (c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName) {
if (c.matchedTransitions && c.matchedTransitions.length > 0) {
c.matchedTransitions.forEach(t => {
const key = buildLinkKey(t.sourceState, t.event);
if (!key || seen.has(key)) return;
seen.add(key);
steps.push({ event: t.event, source: t.sourceState, ambiguous: c.triggerPoint && c.triggerPoint.ambiguous });
});
}
if (c.matchedTransitions && c.matchedTransitions.length > 0) {
c.matchedTransitions.forEach(t => {
const key = buildLinkKey(t.sourceState, t.event);
if (!key || seen.has(key)) return;
seen.add(key);
steps.push({ event: t.event, source: t.sourceState, ambiguous: c.triggerPoint && c.triggerPoint.ambiguous });
});
}
});
@@ -667,11 +687,14 @@
if (chains && chains.length > 0) {
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:10px; border-top:1px solid #eee; padding-top:10px">Triggered by</div>`;
chains.forEach(c => {
const triggerEvent = c.triggerPoint && c.triggerPoint.event;
const lifecycleLabel = triggerEvent && triggerEvent.startsWith('[LIFECYCLE:')
? formatLifecycleLabel(triggerEvent) : null;
const triggerInfo = c.triggerPoint.sourceFile ? ` <span style="color:#94a3b8; font-weight:400; font-size:0.6rem;">(${c.triggerPoint.sourceFile}:${c.triggerPoint.lineNumber})</span>` : '';
const ambiguousLabel = c.triggerPoint.ambiguous ? `<div style="display:inline-block; margin-top:4px; padding:2px 6px; background:#fff7ed; color:#ea580c; border:1px solid #fed7aa; border-radius:4px; font-size:0.6rem; font-weight:700;">⚠️ Ambiguous Resolution (Over-approximated)</div>` : '';
html += `<div style="margin-bottom:20px">
<div style="font-weight:700; font-size:0.85rem; color:var(--text);">${c.entryPoint.name}</div>
<div style="font-size:0.65rem; color:#64748b; font-family:monospace; margin-top:2px;">Trigger: ${c.triggerPoint.methodName}${triggerInfo}</div>
<div style="font-size:0.65rem; color:#64748b; font-family:monospace; margin-top:2px;">${lifecycleLabel ? 'Lifecycle: ' + lifecycleLabel : 'Trigger: ' + c.triggerPoint.methodName}${triggerInfo}</div>
${ambiguousLabel}
${renderParameters(c.entryPoint.parameters)}
<div style="margin-left:10px; border-left:2px solid #e2e8f0; padding-left:12px; margin-top:10px">

View File

@@ -0,0 +1,38 @@
package click.kamil.springstatemachineexporter.html;
import click.kamil.springstatemachineexporter.html.exporter.HtmlExporter;
import click.kamil.springstatemachineexporter.service.ExportService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ExtendedStateMachineLifecycleHtmlTest {
private static final Path EXTENDED_GOLDEN_JSON = Path.of(
"../state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json")
.toAbsolutePath()
.normalize();
@Test
void shouldShowLifecycleRestoreEndpointsInSidebar(@TempDir Path tempDir) throws Exception {
assertThat(EXTENDED_GOLDEN_JSON).exists();
ExportService exportService = new ExportService(List.of(new HtmlExporter()));
exportService.runJsonExporter(EXTENDED_GOLDEN_JSON, tempDir, List.of("html"), List.of());
Path machineDir = tempDir.resolve("click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig");
Path htmlFile = machineDir.resolve("click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig.html");
assertThat(htmlFile).exists();
String html = Files.readString(htmlFile);
assertThat(html).contains("function formatLifecycleLabel");
assertThat(html).contains("POST /api/orders/resume");
assertThat(html).contains("Restore persisted state");
assertThat(html).contains("[LIFECYCLE:RESTORE]");
}
}