From 3a7465f16cbab4bb22501b592c5b0ad6c23a1ac6 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 13 Jul 2025 20:29:58 +0200 Subject: [PATCH] colors for choices in .dot --- build.gradle | 11 +- .../ast/out/Dot.java | 137 +++++++++--------- 2 files changed, 67 insertions(+), 81 deletions(-) diff --git a/build.gradle b/build.gradle index dfe6e5b..0b2c2c5 100644 --- a/build.gradle +++ b/build.gradle @@ -27,17 +27,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0' - // uml model parsing -// implementation 'org.springframework.statemachine:spring-statemachine-uml:3.2.0' - - // deps for SCXMLWriter -// implementation "org.apache.commons:scxml4j:1.0.0" - - implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.36.0' - // deps for parsing java AST - implementation 'com.github.javaparser:javaparser-core:3.27.0' - + implementation 'org.eclipse.jdt:org.eclipse.jdt.core:3.36.0' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/click/kamil/springstatemachineexporter/ast/out/Dot.java b/src/main/java/click/kamil/springstatemachineexporter/ast/out/Dot.java index 128d92b..3d1ad16 100644 --- a/src/main/java/click/kamil/springstatemachineexporter/ast/out/Dot.java +++ b/src/main/java/click/kamil/springstatemachineexporter/ast/out/Dot.java @@ -2,12 +2,14 @@ package click.kamil.springstatemachineexporter.ast.out; import click.kamil.springstatemachineexporter.ast.app.domain.Transition; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; public class Dot implements StateMachineExporter { + private static final String[] CHOICE_COLORS = { + "blue", "darkgreen", "darkred", "darkorange", "darkviolet", "teal", "brown", "crimson" + }; + @Override public String export(List transitions, Set startStates, @@ -21,29 +23,36 @@ public class Dot implements StateMachineExporter { sb.append(" start [shape=point, label=\"\"];\n"); + // Identify choice states and assign colors Set statesWithChoice = new HashSet<>(); + Map choiceColorMap = new HashMap<>(); + int colorIndex = 0; + for (Transition t : transitions) { - if ("withChoice".equals(t.getType())) { + if ("withChoice".equals(t.getType()) && t.getSourceStates() != null) { for (String source : t.getSourceStates()) { - statesWithChoice.add(simplify(source)); + String simplified = simplify(source); + statesWithChoice.add(simplified); + if (!choiceColorMap.containsKey(simplified)) { + choiceColorMap.put(simplified, CHOICE_COLORS[colorIndex % CHOICE_COLORS.length]); + colorIndex++; + } } } } + // Collect all states Set allStates = new HashSet<>(); for (Transition t : transitions) { if (t.getSourceStates() != null) { - for (String s : t.getSourceStates()) { - allStates.add(simplify(s)); - } + for (String s : t.getSourceStates()) allStates.add(simplify(s)); } if (t.getTargetStates() != null) { - for (String tgt : t.getTargetStates()) { - allStates.add(simplify(tgt)); - } + for (String tgt : t.getTargetStates()) allStates.add(simplify(tgt)); } } + // Declare normal states for (String state : allStates) { if (!statesWithChoice.contains(state + "_choice")) { String shape = endStates.contains(state) ? "doublecircle" : "circle"; @@ -52,27 +61,29 @@ public class Dot implements StateMachineExporter { } } + // Declare choice pseudostates for (String state : statesWithChoice) { - String choiceNode = state + "_choice"; - sb.append(" ").append(choiceNode) + sb.append(" ").append(state).append("_choice") .append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n"); } sb.append("\n"); + // Start state arrows for (String start : startStates) { sb.append(" start -> ").append(simplify(start)).append(";\n"); } sb.append("\n"); + // Connect normal states to their choice pseudostates for (String state : statesWithChoice) { - String choiceNode = state + "_choice"; - sb.append(" ").append(state).append(" -> ").append(choiceNode).append(";\n"); + sb.append(" ").append(state).append(" -> ").append(state).append("_choice;\n"); } sb.append("\n"); + // Transitions for (Transition t : transitions) { if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue; @@ -98,99 +109,84 @@ public class Dot implements StateMachineExporter { edgeSource = source + "_choice"; } + // Label: event [guard] / actions + StringBuilder label = new StringBuilder(); + + if (t.getEvent() != null && !t.getEvent().isBlank()) { + label.append(escapeDotString(simplify(t.getEvent()))); + } + // Guard - String guardText = ""; if (t.getGuard() != null && !t.getGuard().isBlank()) { - if (useLambdaGuards && t.isLambdaGuard()) { - guardText = "λ"; - } else { - guardText = escapeDotString( - t.getGuard().replaceAll("[\\n\\r]+", " ") - .replaceAll("\\s{2,}", " ") - .trim() - ); - } + if (!label.isEmpty()) label.append(" "); + String guardText = useLambdaGuards && t.isLambdaGuard() ? "λ" + : escapeDotString(t.getGuard().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim()); + label.append("[").append(guardText).append("]"); } // Actions - String actionsText = ""; if (t.getActions() != null && !t.getActions().isEmpty()) { StringBuilder actionBuilder = new StringBuilder(); for (int i = 0; i < t.getActions().size(); i++) { if (i > 0) actionBuilder.append(", "); String action = t.getActions().get(i); boolean isLambda = i < t.getIsLambdaActions().size() && t.getIsLambdaActions().get(i); - if (useLambdaGuards && isLambda) { - actionBuilder.append("λ"); - } else { - actionBuilder.append(escapeDotString(action)); - } + actionBuilder.append(useLambdaGuards && isLambda ? "λ" : escapeDotString(action)); } - actionsText = actionBuilder.toString(); - } - - // Event and label - StringBuilder label = new StringBuilder(); - if (t.getEvent() != null && !t.getEvent().isBlank()) { - label.append(escapeDotString(simplify(t.getEvent()))); - } - - if (!guardText.isBlank()) { - if (!label.isEmpty()) label.append(" "); - label.append("[").append(guardText).append("]"); - } - - if (!actionsText.isBlank()) { if (!label.isEmpty()) label.append(" / "); - label.append(actionsText); + label.append(actionBuilder); } - if (("withChoice".equals(t.getType()) || "withJunction".equals(t.getType())) && t.getOrder() != null) { + // Order + if ((t.getType().equals("withChoice") || t.getType().equals("withJunction")) && t.getOrder() != null) { if (!label.isEmpty()) label.append(" "); label.append("(order=").append(t.getOrder()).append(")"); } - // Build edge attributes - StringBuilder edgeAttributes = new StringBuilder(); + // Edge attributes + StringBuilder edgeAttrs = new StringBuilder(); if (!label.isEmpty()) { - appendEdgeAttr(edgeAttributes, "label", label.toString()); + appendEdgeAttr(edgeAttrs, "label", label.toString()); } + // Style/color by type switch (t.getType()) { case "withInternal": - appendEdgeAttr(edgeAttributes, "style", "dashed"); - appendEdgeAttr(edgeAttributes, "color", "gray"); + appendEdgeAttr(edgeAttrs, "style", "dashed"); + appendEdgeAttr(edgeAttrs, "color", "gray"); break; case "withLocal": - appendEdgeAttr(edgeAttributes, "style", "dotted"); - appendEdgeAttr(edgeAttributes, "color", "darkgray"); + appendEdgeAttr(edgeAttrs, "style", "dotted"); + appendEdgeAttr(edgeAttrs, "color", "darkgray"); break; case "withChoice": - appendEdgeAttr(edgeAttributes, "style", "solid"); - appendEdgeAttr(edgeAttributes, "color", "blue"); + String choiceColor = choiceColorMap.getOrDefault(source, "blue"); + appendEdgeAttr(edgeAttrs, "style", "solid"); + appendEdgeAttr(edgeAttrs, "color", choiceColor); break; case "withJunction": - appendEdgeAttr(edgeAttributes, "style", "dotted"); - appendEdgeAttr(edgeAttributes, "color", "purple"); + appendEdgeAttr(edgeAttrs, "style", "dotted"); + appendEdgeAttr(edgeAttrs, "color", "purple"); break; case "withFork": - appendEdgeAttr(edgeAttributes, "style", "bold"); - appendEdgeAttr(edgeAttributes, "color", "green"); + appendEdgeAttr(edgeAttrs, "style", "bold"); + appendEdgeAttr(edgeAttrs, "color", "green"); break; case "withJoin": - appendEdgeAttr(edgeAttributes, "style", "bold"); - appendEdgeAttr(edgeAttributes, "color", "darkorange"); + appendEdgeAttr(edgeAttrs, "style", "bold"); + appendEdgeAttr(edgeAttrs, "color", "darkorange"); break; case "withExternal": default: - appendEdgeAttr(edgeAttributes, "style", "solid"); - appendEdgeAttr(edgeAttributes, "color", "black"); - break; + appendEdgeAttr(edgeAttrs, "style", "solid"); + appendEdgeAttr(edgeAttrs, "color", "black"); } - String edgeLabel = !edgeAttributes.isEmpty() ? " [" + edgeAttributes + "]" : ""; - - sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n"); + sb.append(" ").append(edgeSource).append(" -> ").append(target); + if (!edgeAttrs.isEmpty()) { + sb.append(" [").append(edgeAttrs).append("]"); + } + sb.append(";\n"); } } } @@ -206,8 +202,7 @@ public class Dot implements StateMachineExporter { private String escapeDotString(String input) { if (input == null) return ""; - return input.replace("\\", "\\\\") - .replace("\"", "\\\""); + return input.replace("\\", "\\\\").replace("\"", "\\\""); } private void appendEdgeAttr(StringBuilder sb, String key, String value) {