From 12183693aa3f7c75562ebd03d62eeae5a08b5e0a Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 7 Jul 2026 21:15:12 +0200 Subject: [PATCH] cursor --- .../enricher/TransitionLinkerEnricher.java | 40 ++++++++++---- .../TransitionLinkerEnricherTest.java | 2 +- .../src/main/resources/html/template.html | 52 +++++++++++++------ .../html/HtmlExporterTest.java | 7 +-- 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index 7f17edf..b4fa03c 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import click.kamil.springstatemachineexporter.exporter.ExportOptions; import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine; import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine; import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine; @@ -21,6 +22,8 @@ import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictF public class TransitionLinkerEnricher implements AnalysisEnricher { + private static final ExportOptions LINK_FORMAT_OPTIONS = ExportOptions.builder().build(); + private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine(); private final EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(); @@ -47,17 +50,16 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { for (Transition t : stateMachineTransitions) { if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) { - String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName(); - // Event matches + String smEventForLink = formatEventForLink(t.getEvent()); for (State smSourceState : t.getSourceStates()) { - String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName(); - String smSource = simplify(smSourceRaw); + String smSourceForLink = formatSourceForLink(smSourceState); + String smSource = simplify(smSourceForLink); if (triggerSource == null || triggerSource.equals(smSource)) { if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) { MatchedTransition mt = MatchedTransition.builder() - .sourceState(smSourceRaw) - .targetState(smSourceRaw) - .event(smEventRaw) + .sourceState(smSourceForLink) + .targetState(smSourceForLink) + .event(smEventForLink) .build(); if (isRoutedToCorrectMachine(chain, result.getName(), context) && isConstraintCompatible(tp.getConstraint(), result.getName())) { @@ -65,11 +67,11 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { } } else { for (State smTargetState : t.getTargetStates()) { - String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName(); + String targetForLink = formatSourceForLink(smTargetState); MatchedTransition mt = MatchedTransition.builder() - .sourceState(smSourceRaw) - .targetState(targetRaw) - .event(smEventRaw) + .sourceState(smSourceForLink) + .targetState(targetForLink) + .event(smEventForLink) .build(); if (isRoutedToCorrectMachine(chain, result.getName(), context) && isConstraintCompatible(tp.getConstraint(), result.getName())) { @@ -215,6 +217,22 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { return simplified; } + private String formatSourceForLink(State state) { + if (state == null) return null; + return simplifyForLink(LINK_FORMAT_OPTIONS.formatState(state)); + } + + private String formatEventForLink(click.kamil.springstatemachineexporter.model.Event event) { + if (event == null) return null; + return LINK_FORMAT_OPTIONS.formatEvent(event); + } + + private String simplifyForLink(String name) { + if (name == null) return ""; + if (name.contains("\"")) return name; + return name.replaceAll("[^a-zA-Z0-9_.]", ""); + } + private boolean isGuardedContains(String smEvent, String triggerEvent) { if (smEvent == null || triggerEvent == null) return false; diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java index 22965f0..4e65a42 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java @@ -173,7 +173,7 @@ class TransitionLinkerEnricherTest { CallChain updatedChain = result.getMetadata().getCallChains().get(0); assertThat(updatedChain.getMatchedTransitions()).hasSize(1); - assertThat(updatedChain.getMatchedTransitions().get(0).getEvent()).isEqualTo("com.example.OrderEvents.PAY"); + assertThat(updatedChain.getMatchedTransitions().get(0).getEvent()).isEqualTo("OrderEvents.PAY"); } @Test diff --git a/state_machine_exporter_html/src/main/resources/html/template.html b/state_machine_exporter_html/src/main/resources/html/template.html index d227765..f39bafe 100644 --- a/state_machine_exporter_html/src/main/resources/html/template.html +++ b/state_machine_exporter_html/src/main/resources/html/template.html @@ -429,11 +429,15 @@ clearHighlights(); const chains = metadata.metadata.callChains || []; + 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 }); }); } @@ -459,22 +463,22 @@ }); steps.forEach(step => { - let targetEvent = ""; - let targetSource = ""; + let targetLinkKey = ""; let targetAnon = ""; let isAmbiguous = false; if (typeof step === 'object') { - targetEvent = normalize(step.event); - targetSource = normalize(step.source); + targetLinkKey = buildLinkKey(step.source, step.event); isAmbiguous = step.ambiguous; } else if (step.includes("->")) { const parts = step.split("->"); targetAnon = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]); } else { - targetEvent = normalize(step); + targetLinkKey = "__" + normalize(formatForLinkId(step)); } + if (!targetLinkKey && !targetAnon) return; + svg.querySelectorAll('a').forEach(link => { const href = link.getAttribute('xlink:href') || link.getAttribute('href'); if (!href) return; @@ -482,14 +486,11 @@ let matches = false; if (targetAnon) { matches = (href === targetAnon); - } else if (targetEvent) { - if (targetSource) { - // Exact match - matches = (href === "#link_" + targetSource + "__" + targetEvent); - } else { - // Match all sources for this event - matches = href.startsWith("#link_") && href.endsWith("__" + targetEvent); - } + } else if (targetLinkKey.includes("__")) { + matches = (href === "#link_" + targetLinkKey); + } else { + // Business-flow step: event name only, match that event from any source + matches = href.startsWith("#link_") && href.endsWith(targetLinkKey); } if (matches) { @@ -570,8 +571,7 @@ if (!t.event) return false; const evStr = t.event.fullIdentifier || t.event.rawName || t.event; const sourceStr = t.sourceStates && t.sourceStates.length > 0 ? (t.sourceStates[0].fullIdentifier || t.sourceStates[0].rawName) : ""; - const expectedLinkId = normalize(sourceStr) + "__" + normalize(evStr); - return expectedLinkId === linkId; + return buildLinkKey(sourceStr, evStr) === linkId; }); if (metaTrans && metaTrans.event) { cleanEventName = metaTrans.event.fullIdentifier || metaTrans.event.rawName || metaTrans.event; @@ -602,17 +602,37 @@ }); } + function formatForLinkId(identifier) { + if (!identifier) return ""; + const s = String(identifier); + const lastDot = s.lastIndexOf('.'); + if (lastDot <= 0) return s; + const prevDot = s.lastIndexOf('.', lastDot - 1); + return prevDot > 0 ? s.substring(prevDot + 1) : s; + } + function normalize(s) { if (!s) return ""; return s.replace(/[^a-zA-Z0-9]/g, '_'); } + function buildLinkKey(source, event) { + if (!event) return ""; + const formattedEvent = formatForLinkId(event); + if (!source) return "__" + normalize(formattedEvent); + return normalize(formatForLinkId(source)) + "__" + normalize(formattedEvent); + } + + function eventsMatchForLink(a, b) { + return normalize(formatForLinkId(a)) === normalize(formatForLinkId(b)); + } + function createTooltip(name, trans) { const chains = (metadata.metadata.callChains || []).filter(c => { if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false; return c.matchedTransitions.some(t => { const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event; - return normalize(cEventStr) === normalize(name); + return eventsMatchForLink(cEventStr, name); }); }); diff --git a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java index 5d17dcb..f7301e0 100644 --- a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java +++ b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/HtmlExporterTest.java @@ -156,10 +156,7 @@ public class HtmlExporterTest { String html = exporter.export(result, ExportOptions.builder().build()); - // Verify that the template gracefully unwraps the Event JSON object for interactive hovers in attachInteractivity - assertThat(html).contains("const evStr = t.event.fullIdentifier || t.event.rawName || t.event;"); - - // Verify that the template gracefully unwraps the Event JSON object when filtering call chains in createTooltip - assertThat(html).contains("const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event;"); + assertThat(html).contains("function formatForLinkId(identifier)"); + assertThat(html).contains("function buildLinkKey(source, event)"); } }