cursor
This commit is contained in:
@@ -14,6 +14,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
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.HeuristicBeanResolutionEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
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 {
|
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
private static final ExportOptions LINK_FORMAT_OPTIONS = ExportOptions.builder().build();
|
||||||
|
|
||||||
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
||||||
private final EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine();
|
private final EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine();
|
||||||
|
|
||||||
@@ -47,17 +50,16 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
|
|
||||||
for (Transition t : stateMachineTransitions) {
|
for (Transition t : stateMachineTransitions) {
|
||||||
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
||||||
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
|
String smEventForLink = formatEventForLink(t.getEvent());
|
||||||
// Event matches
|
|
||||||
for (State smSourceState : t.getSourceStates()) {
|
for (State smSourceState : t.getSourceStates()) {
|
||||||
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
String smSourceForLink = formatSourceForLink(smSourceState);
|
||||||
String smSource = simplify(smSourceRaw);
|
String smSource = simplify(smSourceForLink);
|
||||||
if (triggerSource == null || triggerSource.equals(smSource)) {
|
if (triggerSource == null || triggerSource.equals(smSource)) {
|
||||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
MatchedTransition mt = MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
.sourceState(smSourceRaw)
|
.sourceState(smSourceForLink)
|
||||||
.targetState(smSourceRaw)
|
.targetState(smSourceForLink)
|
||||||
.event(smEventRaw)
|
.event(smEventForLink)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||||
@@ -65,11 +67,11 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (State smTargetState : t.getTargetStates()) {
|
for (State smTargetState : t.getTargetStates()) {
|
||||||
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
String targetForLink = formatSourceForLink(smTargetState);
|
||||||
MatchedTransition mt = MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
.sourceState(smSourceRaw)
|
.sourceState(smSourceForLink)
|
||||||
.targetState(targetRaw)
|
.targetState(targetForLink)
|
||||||
.event(smEventRaw)
|
.event(smEventForLink)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||||
@@ -215,6 +217,22 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
return simplified;
|
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) {
|
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||||
if (smEvent == null || triggerEvent == null) return false;
|
if (smEvent == null || triggerEvent == null) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class TransitionLinkerEnricherTest {
|
|||||||
|
|
||||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
|
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
|
@Test
|
||||||
|
|||||||
@@ -429,11 +429,15 @@
|
|||||||
clearHighlights();
|
clearHighlights();
|
||||||
const chains = metadata.metadata.callChains || [];
|
const chains = metadata.metadata.callChains || [];
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
let steps = [];
|
let steps = [];
|
||||||
chains.forEach(c => {
|
chains.forEach(c => {
|
||||||
if (c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName) {
|
if (c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName) {
|
||||||
if (c.matchedTransitions && c.matchedTransitions.length > 0) {
|
if (c.matchedTransitions && c.matchedTransitions.length > 0) {
|
||||||
c.matchedTransitions.forEach(t => {
|
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 });
|
steps.push({ event: t.event, source: t.sourceState, ambiguous: c.triggerPoint && c.triggerPoint.ambiguous });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -459,22 +463,22 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
steps.forEach(step => {
|
steps.forEach(step => {
|
||||||
let targetEvent = "";
|
let targetLinkKey = "";
|
||||||
let targetSource = "";
|
|
||||||
let targetAnon = "";
|
let targetAnon = "";
|
||||||
let isAmbiguous = false;
|
let isAmbiguous = false;
|
||||||
|
|
||||||
if (typeof step === 'object') {
|
if (typeof step === 'object') {
|
||||||
targetEvent = normalize(step.event);
|
targetLinkKey = buildLinkKey(step.source, step.event);
|
||||||
targetSource = normalize(step.source);
|
|
||||||
isAmbiguous = step.ambiguous;
|
isAmbiguous = step.ambiguous;
|
||||||
} else if (step.includes("->")) {
|
} else if (step.includes("->")) {
|
||||||
const parts = step.split("->");
|
const parts = step.split("->");
|
||||||
targetAnon = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
|
targetAnon = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
|
||||||
} else {
|
} else {
|
||||||
targetEvent = normalize(step);
|
targetLinkKey = "__" + normalize(formatForLinkId(step));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!targetLinkKey && !targetAnon) return;
|
||||||
|
|
||||||
svg.querySelectorAll('a').forEach(link => {
|
svg.querySelectorAll('a').forEach(link => {
|
||||||
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
||||||
if (!href) return;
|
if (!href) return;
|
||||||
@@ -482,14 +486,11 @@
|
|||||||
let matches = false;
|
let matches = false;
|
||||||
if (targetAnon) {
|
if (targetAnon) {
|
||||||
matches = (href === targetAnon);
|
matches = (href === targetAnon);
|
||||||
} else if (targetEvent) {
|
} else if (targetLinkKey.includes("__")) {
|
||||||
if (targetSource) {
|
matches = (href === "#link_" + targetLinkKey);
|
||||||
// Exact match
|
|
||||||
matches = (href === "#link_" + targetSource + "__" + targetEvent);
|
|
||||||
} else {
|
} else {
|
||||||
// Match all sources for this event
|
// Business-flow step: event name only, match that event from any source
|
||||||
matches = href.startsWith("#link_") && href.endsWith("__" + targetEvent);
|
matches = href.startsWith("#link_") && href.endsWith(targetLinkKey);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches) {
|
if (matches) {
|
||||||
@@ -570,8 +571,7 @@
|
|||||||
if (!t.event) return false;
|
if (!t.event) return false;
|
||||||
const evStr = t.event.fullIdentifier || t.event.rawName || t.event;
|
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 sourceStr = t.sourceStates && t.sourceStates.length > 0 ? (t.sourceStates[0].fullIdentifier || t.sourceStates[0].rawName) : "";
|
||||||
const expectedLinkId = normalize(sourceStr) + "__" + normalize(evStr);
|
return buildLinkKey(sourceStr, evStr) === linkId;
|
||||||
return expectedLinkId === linkId;
|
|
||||||
});
|
});
|
||||||
if (metaTrans && metaTrans.event) {
|
if (metaTrans && metaTrans.event) {
|
||||||
cleanEventName = metaTrans.event.fullIdentifier || metaTrans.event.rawName || 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) {
|
function normalize(s) {
|
||||||
if (!s) return "";
|
if (!s) return "";
|
||||||
return s.replace(/[^a-zA-Z0-9]/g, '_');
|
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) {
|
function createTooltip(name, trans) {
|
||||||
const chains = (metadata.metadata.callChains || []).filter(c => {
|
const chains = (metadata.metadata.callChains || []).filter(c => {
|
||||||
if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false;
|
if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false;
|
||||||
return c.matchedTransitions.some(t => {
|
return c.matchedTransitions.some(t => {
|
||||||
const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event;
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,10 +156,7 @@ public class HtmlExporterTest {
|
|||||||
|
|
||||||
String html = exporter.export(result, ExportOptions.builder().build());
|
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("function formatForLinkId(identifier)");
|
||||||
assertThat(html).contains("const evStr = t.event.fullIdentifier || t.event.rawName || t.event;");
|
assertThat(html).contains("function buildLinkKey(source, 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;");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user