fqn names of events and states

This commit is contained in:
2026-06-18 22:00:44 +02:00
parent 0c9e8de310
commit 5894303510
38 changed files with 1122 additions and 286 deletions

View File

@@ -60,7 +60,7 @@ public class AnalysisResult {
if (transitions != null) {
for (var t : transitions) {
if (t.getEvent() != null) {
t.setEvent(resolver.resolveValue(t.getEvent(), properties));
t.setEvent(click.kamil.springstatemachineexporter.model.Event.of(t.getEvent().rawName(), resolver.resolveValue(t.getEvent().fullIdentifier(), properties)));
}
// Resolve states within transitions
t.setSourceStates(t.getSourceStates().stream()

View File

@@ -4,6 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver
import click.kamil.springstatemachineexporter.model.Action;
import click.kamil.springstatemachineexporter.model.Guard;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
@@ -213,9 +214,30 @@ public class AstTransitionParser {
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
String eventValue = constantResolver.resolve(resolved, context);
if (eventValue != null) {
t.setEvent(eventValue);
t.setEvent(Event.of(resolved.toString(), eventValue));
} else if (resolved instanceof QualifiedName qn) {
String qualifier = qn.getQualifier().toString();
String name = qn.getName().getIdentifier();
TypeDeclaration td = context.getTypeDeclaration(qualifier, cu);
if (td != null) {
t.setEvent(Event.of(resolved.toString(), context.getFqn(td) + "." + name));
} else {
t.setEvent(Event.of(resolved.toString(), qn.getFullyQualifiedName()));
}
} else if (resolved instanceof SimpleName sn) {
String name = sn.getIdentifier();
String full = name;
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + name)) {
full = impName;
break;
}
}
t.setEvent(Event.of(resolved.toString(), full));
} else {
t.setEvent(QuotedExpression.of(resolved).toStringWithoutQuotes());
t.setEvent(Event.of(QuotedExpression.of(resolved).toStringWithoutQuotes()));
}
}
case "guard" -> {

View File

@@ -48,6 +48,12 @@ public class ExporterCommand implements Callable<Integer> {
@Option(names = {"-p", "--profiles"}, description = "Spring profiles to activate for property resolution.", split = ",")
private List<String> activeProfiles;
@Option(names = {"--event"}, description = "Format for events when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
private click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat;
@Option(names = {"--state"}, description = "Format for states when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
private click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat;
@Override
public Integer call() throws Exception {
var out = spec.commandLine().getOut();
@@ -71,9 +77,9 @@ public class ExporterCommand implements Callable<Integer> {
try {
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
if (jsonFile != null) {
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles);
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
} else {
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles);
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, eventFormat, stateFormat);
}
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
return 0;

View File

@@ -41,7 +41,7 @@ public class Dot implements StateMachineExporter {
for (Transition t : transitions) {
if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) {
for (State source : t.getSourceStates()) {
String simplified = simplify(source.toString());
String simplified = simplify(options.formatState(source));
statesWithChoice.add(simplified);
if (!choiceColorMap.containsKey(simplified)) {
choiceColorMap.put(simplified, CHOICE_COLORS[colorIndex % CHOICE_COLORS.length]);
@@ -86,11 +86,11 @@ public class Dot implements StateMachineExporter {
}
for (State rawSourceState : t.getSourceStates()) {
String rawSource = rawSourceState.toString();
String rawSource = options.formatState(rawSourceState);
String source = simplify(rawSource);
for (State rawTargetState : targets) {
String rawTarget = rawTargetState.toString();
String rawTarget = options.formatState(rawTargetState);
String target = simplify(rawTarget);
String edgeSource = source;
@@ -100,8 +100,11 @@ public class Dot implements StateMachineExporter {
// Label: event [guard] / actions
StringBuilder label = new StringBuilder();
if (t.getEvent() != null && !t.getEvent().isBlank()) {
label.append(escapeDotString(t.getEvent()));
if (t.getEvent() != null) {
String eventStr = options.formatEvent(t.getEvent());
if (eventStr != null && !eventStr.isBlank()) {
label.append(escapeDotString(eventStr));
}
}
// Guard

View File

@@ -0,0 +1,5 @@
package click.kamil.springstatemachineexporter.exporter;
public enum EnumFormat {
fn, fqn, sn
}

View File

@@ -12,4 +12,35 @@ public class ExportOptions {
boolean renderChoicesAsDiamonds = true;
@Builder.Default
boolean embedIdentifiers = false;
@Builder.Default
EnumFormat eventFormat = EnumFormat.fn;
@Builder.Default
EnumFormat stateFormat = EnumFormat.fn;
public String formatState(click.kamil.springstatemachineexporter.model.State state) {
if (state == null) return null;
return format(state.rawName(), state.fullIdentifier(), stateFormat);
}
public String formatEvent(click.kamil.springstatemachineexporter.model.Event event) {
if (event == null) return null;
return format(event.rawName(), event.fullIdentifier(), eventFormat);
}
private String format(String raw, String fqn, EnumFormat format) {
if (fqn == null) return raw;
switch (format) {
case fqn: return fqn;
case sn: return fqn.substring(fqn.lastIndexOf('.') + 1);
case fn:
int lastDot = fqn.lastIndexOf('.');
if (lastDot > 0) {
int prevDot = fqn.lastIndexOf('.', lastDot - 1);
return prevDot > 0 ? fqn.substring(prevDot + 1) : fqn;
}
return fqn;
default: return raw;
}
}
}

View File

@@ -63,7 +63,7 @@ public class PlantUml implements StateMachineExporter {
for (Transition t : transitions) {
if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) {
for (State source : t.getSourceStates()) {
statesWithChoice.add(simplify(source.toString()));
statesWithChoice.add(simplify(options.formatState(source)));
}
}
}
@@ -77,7 +77,7 @@ public class PlantUml implements StateMachineExporter {
Set<String> junctionStates = transitions.stream()
.filter(t -> t.getType() == TransitionType.JUNCTION)
.flatMap(t -> t.getSourceStates().stream())
.map(s -> simplify(s.toString()))
.map(s -> simplify(options.formatState(s)))
.collect(Collectors.toCollection(java.util.LinkedHashSet::new));
for (String junction : junctionStates) {
if (options.isRenderChoicesAsDiamonds()) {
@@ -96,16 +96,16 @@ public class PlantUml implements StateMachineExporter {
List<String> targets;
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
if (!allowNoTarget) {
targets = t.getSourceStates().stream().map(s -> s.toString()).toList();
targets = t.getSourceStates().stream().map(s -> options.formatState(s)).toList();
} else {
continue;
}
} else {
targets = t.getTargetStates().stream().map(s -> s.toString()).toList();
targets = t.getTargetStates().stream().map(s -> options.formatState(s)).toList();
}
for (State rawSourceState : t.getSourceStates()) {
String source = simplify(rawSourceState.toString());
String source = simplify(options.formatState(rawSourceState));
for (String rawTarget : targets) {
String target = simplify(rawTarget);
@@ -116,13 +116,17 @@ public class PlantUml implements StateMachineExporter {
sb.append(" <<").append(styleClass).append(">>");
if (t.getEvent() != null && !t.getEvent().isBlank()) {
sb.append(" <<e_").append(normalize(t.getEvent())).append(">>");
if (t.getEvent() != null) {
String eventStr = options.formatEvent(t.getEvent());
if (eventStr != null && !eventStr.isBlank()) {
sb.append(" <<e_").append(normalize(eventStr)).append(">>");
}
}
String label = buildLabel(t, options.isUseLambdaGuards());
String label = buildLabel(t, options);
if (options.isEmbedIdentifiers()) {
String linkId = t.getEvent() != null ? "link_" + normalize(t.getEvent()) : "link_anon_" + normalize(source) + "_" + normalize(target);
String eventStr = t.getEvent() != null ? options.formatEvent(t.getEvent()) : null;
String linkId = eventStr != null && !eventStr.isBlank() ? "link_" + normalize(eventStr) : "link_anon_" + normalize(source) + "_" + normalize(target);
// Force a label even if empty to ensure the link group exists in SVG
String displayLabel = label.isEmpty() ? " " : label;
// Brackets [...] in the label break PlantUML link syntax [[url label]],
@@ -191,14 +195,17 @@ public class PlantUml implements StateMachineExporter {
return state.replaceAll("[^a-zA-Z0-9_.]", "");
}
private String buildLabel(Transition t, boolean useLambdaGuards) {
private String buildLabel(Transition t, ExportOptions options) {
List<String> parts = new ArrayList<>();
if (t.getEvent() != null && !t.getEvent().isBlank()) {
parts.add(t.getEvent());
if (t.getEvent() != null) {
String eventStr = options.formatEvent(t.getEvent());
if (eventStr != null && !eventStr.isBlank()) {
parts.add(eventStr);
}
}
if (t.getGuard() != null) {
String g = t.getGuard().expression();
if (useLambdaGuards && t.getGuard().isLambda()) {
if (options.isUseLambdaGuards() && t.getGuard().isLambda()) {
parts.add("[" + LAMBDA + "]");
} else {
parts.add("[" + g.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim() + "]");
@@ -206,7 +213,7 @@ public class PlantUml implements StateMachineExporter {
}
if (t.getActions() != null && !t.getActions().isEmpty()) {
parts.add("/ " + t.getActions().stream()
.map(a -> (useLambdaGuards && a.isLambda()) ? LAMBDA : a.expression().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim())
.map(a -> (options.isUseLambdaGuards() && a.isLambda()) ? LAMBDA : a.expression().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim())
.collect(Collectors.joining(", ")));
}
Optional.ofNullable(t.getOrder())

View File

@@ -31,8 +31,8 @@ public class Scxml implements StateMachineExporter {
Set<String> allStates = new java.util.LinkedHashSet<>();
for (Transition t : transitions) {
t.getSourceStates().forEach(s -> allStates.add(s.toString()));
t.getTargetStates().forEach(s -> allStates.add(s.toString()));
t.getSourceStates().forEach(s -> allStates.add(options.formatState(s)));
t.getTargetStates().forEach(s -> allStates.add(options.formatState(s)));
}
allStates.addAll(startStates);
allStates.addAll(endStates);
@@ -41,7 +41,7 @@ public class Scxml implements StateMachineExporter {
sb.append(" <state id=\"").append(simplify(state)).append("\">\n");
for (Transition t : transitions) {
if (t.getSourceStates() == null || t.getSourceStates().stream().noneMatch(s -> s.toString().equals(state)))
if (t.getSourceStates() == null || t.getSourceStates().stream().noneMatch(s -> options.formatState(s).equals(state)))
continue;
boolean allowNoTarget = t.getType() == TransitionType.JOIN || t.getType() == TransitionType.FORK;
@@ -58,7 +58,7 @@ public class Scxml implements StateMachineExporter {
}
for (State target : targets) {
sb.append(renderTransition(t, target, options.isUseLambdaGuards()));
sb.append(renderTransition(t, target, options));
}
}
sb.append(" </state>\n");
@@ -73,15 +73,18 @@ public class Scxml implements StateMachineExporter {
return ".scxml.xml";
}
private String renderTransition(Transition t, State target, boolean useLambdaGuards) {
private String renderTransition(Transition t, State target, ExportOptions options) {
StringBuilder sb = new StringBuilder();
sb.append(" <transition target=\"").append(simplify(target.toString())).append("\"");
sb.append(" <transition target=\"").append(simplify(options.formatState(target))).append("\"");
if (t.getEvent() != null && !t.getEvent().isBlank()) {
sb.append(" event=\"").append(escapeXml(t.getEvent())).append("\"");
if (t.getEvent() != null) {
String eventStr = options.formatEvent(t.getEvent());
if (eventStr != null && !eventStr.isBlank()) {
sb.append(" event=\"").append(escapeXml(eventStr)).append("\"");
}
}
String guard = getGuardText(t, useLambdaGuards);
String guard = getGuardText(t, options.isUseLambdaGuards());
if (!guard.isBlank()) {
sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
}

View File

@@ -0,0 +1,16 @@
package click.kamil.springstatemachineexporter.model;
public record Event(String rawName, String fullIdentifier) {
public static Event of(String name) {
return new Event(name, name);
}
public static Event of(String raw, String full) {
return new Event(raw, full);
}
@Override
public String toString() {
return fullIdentifier != null ? fullIdentifier : rawName;
}
}

View File

@@ -14,7 +14,7 @@ public class Transition {
private TransitionType type;
private List<State> sourceStates = new ArrayList<>();
private List<State> targetStates = new ArrayList<>();
private String event;
private Event event;
private Guard guard;
private List<Action> actions = new ArrayList<>();

View File

@@ -52,14 +52,22 @@ public class ExportService {
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList());
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null);
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
CodebaseContext context = new CodebaseContext();
context.setActiveProfiles(activeProfiles);
@@ -116,21 +124,25 @@ public class ExportService {
context.scan(allPaths, Collections.emptySet());
CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds, flows, machineFilter, activeProfiles != null ? activeProfiles : Collections.emptyList());
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds, flows, machineFilter, activeProfiles != null ? activeProfiles : Collections.emptyList(), eventFormat, stateFormat);
}
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
runJsonExporter(jsonFile, outputDir, selectedFormats, Collections.emptyList());
runJsonExporter(jsonFile, outputDir, selectedFormats, Collections.emptyList(), click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
}
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles) throws IOException {
runJsonExporter(jsonFile, outputDir, selectedFormats, activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
}
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
JsonImportService jsonImportService = new JsonImportService();
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
// Apply property resolution pass if placeholders exist
resolveProperties(result, activeProfiles);
generateOutputs(outputDir, result, selectedFormats);
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
}
private void resolveProperties(AnalysisResult result, List<String> activeProfiles) {
@@ -154,7 +166,7 @@ public class ExportService {
result.applyResolution(merged);
}
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter, List<String> activeProfiles) throws IOException {
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
Set<String> processedLocations = new HashSet<>();
// 1. Find entry point classes (annotated or extending adapter)
@@ -163,7 +175,7 @@ public class ExportService {
String fqn = context.getFqn(td);
if (machineFilter != null && !fqn.contains(machineFilter)) continue;
if (processedLocations.add(fqn)) {
processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows, activeProfiles);
processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows, activeProfiles, eventFormat, stateFormat);
}
}
@@ -177,12 +189,12 @@ public class ExportService {
if (machineFilter != null && !uniqueId.contains(machineFilter)) continue;
if (processedLocations.add(uniqueId)) {
processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows, activeProfiles);
processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows, activeProfiles, eventFormat, stateFormat);
}
}
}
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles) throws IOException {
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
String className = context.getFqn(td);
log.info("Processing state machine config: {}", className);
@@ -214,10 +226,10 @@ public class ExportService {
resolveProperties(result, activeProfiles);
generateOutputs(outputDir, result, selectedFormats);
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
}
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles) throws IOException {
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
String parentFqn = context.getFqn(parentClass);
String methodName = m.getName().getIdentifier();
@@ -245,10 +257,10 @@ public class ExportService {
resolveProperties(result, activeProfiles);
generateOutputs(outputDir, result, selectedFormats);
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
}
private void generateOutputs(Path outputDir, AnalysisResult result, List<String> selectedFormats) throws IOException {
private void generateOutputs(Path outputDir, AnalysisResult result, List<String> selectedFormats, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
if (selectedFormats == null || selectedFormats.isEmpty()) {
return;
}
@@ -259,6 +271,8 @@ public class ExportService {
ExportOptions options = ExportOptions.builder()
.renderChoicesAsDiamonds(result.isRenderChoicesAsDiamonds())
.eventFormat(eventFormat)
.stateFormat(stateFormat)
.build();
for (StateMachineExporter exporter : exporters) {