colors for choices in .dot
This commit is contained in:
11
build.gradle
11
build.gradle
@@ -27,17 +27,8 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter'
|
implementation 'org.springframework.boot:spring-boot-starter'
|
||||||
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
|
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
|
// 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'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package click.kamil.springstatemachineexporter.ast.out;
|
|||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class Dot implements StateMachineExporter {
|
public class Dot implements StateMachineExporter {
|
||||||
|
|
||||||
|
private static final String[] CHOICE_COLORS = {
|
||||||
|
"blue", "darkgreen", "darkred", "darkorange", "darkviolet", "teal", "brown", "crimson"
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String export(List<Transition> transitions,
|
public String export(List<Transition> transitions,
|
||||||
Set<String> startStates,
|
Set<String> startStates,
|
||||||
@@ -21,29 +23,36 @@ public class Dot implements StateMachineExporter {
|
|||||||
|
|
||||||
sb.append(" start [shape=point, label=\"\"];\n");
|
sb.append(" start [shape=point, label=\"\"];\n");
|
||||||
|
|
||||||
|
// Identify choice states and assign colors
|
||||||
Set<String> statesWithChoice = new HashSet<>();
|
Set<String> statesWithChoice = new HashSet<>();
|
||||||
|
Map<String, String> choiceColorMap = new HashMap<>();
|
||||||
|
int colorIndex = 0;
|
||||||
|
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if ("withChoice".equals(t.getType())) {
|
if ("withChoice".equals(t.getType()) && t.getSourceStates() != null) {
|
||||||
for (String source : t.getSourceStates()) {
|
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<String> allStates = new HashSet<>();
|
Set<String> allStates = new HashSet<>();
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (t.getSourceStates() != null) {
|
if (t.getSourceStates() != null) {
|
||||||
for (String s : t.getSourceStates()) {
|
for (String s : t.getSourceStates()) allStates.add(simplify(s));
|
||||||
allStates.add(simplify(s));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (t.getTargetStates() != null) {
|
if (t.getTargetStates() != null) {
|
||||||
for (String tgt : t.getTargetStates()) {
|
for (String tgt : t.getTargetStates()) allStates.add(simplify(tgt));
|
||||||
allStates.add(simplify(tgt));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Declare normal states
|
||||||
for (String state : allStates) {
|
for (String state : allStates) {
|
||||||
if (!statesWithChoice.contains(state + "_choice")) {
|
if (!statesWithChoice.contains(state + "_choice")) {
|
||||||
String shape = endStates.contains(state) ? "doublecircle" : "circle";
|
String shape = endStates.contains(state) ? "doublecircle" : "circle";
|
||||||
@@ -52,27 +61,29 @@ public class Dot implements StateMachineExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Declare choice pseudostates
|
||||||
for (String state : statesWithChoice) {
|
for (String state : statesWithChoice) {
|
||||||
String choiceNode = state + "_choice";
|
sb.append(" ").append(state).append("_choice")
|
||||||
sb.append(" ").append(choiceNode)
|
|
||||||
.append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n");
|
.append(" [shape=diamond, label=\"\", fillcolor=lightyellow];\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
|
// Start state arrows
|
||||||
for (String start : startStates) {
|
for (String start : startStates) {
|
||||||
sb.append(" start -> ").append(simplify(start)).append(";\n");
|
sb.append(" start -> ").append(simplify(start)).append(";\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
|
// Connect normal states to their choice pseudostates
|
||||||
for (String state : statesWithChoice) {
|
for (String state : statesWithChoice) {
|
||||||
String choiceNode = state + "_choice";
|
sb.append(" ").append(state).append(" -> ").append(state).append("_choice;\n");
|
||||||
sb.append(" ").append(state).append(" -> ").append(choiceNode).append(";\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
|
// Transitions
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
|
if (t.getSourceStates() == null || t.getSourceStates().isEmpty()) continue;
|
||||||
|
|
||||||
@@ -98,99 +109,84 @@ public class Dot implements StateMachineExporter {
|
|||||||
edgeSource = source + "_choice";
|
edgeSource = source + "_choice";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard
|
// Label: event [guard] / actions
|
||||||
String guardText = "";
|
StringBuilder label = new StringBuilder();
|
||||||
if (t.getGuard() != null && !t.getGuard().isBlank()) {
|
|
||||||
if (useLambdaGuards && t.isLambdaGuard()) {
|
if (t.getEvent() != null && !t.getEvent().isBlank()) {
|
||||||
guardText = "λ";
|
label.append(escapeDotString(simplify(t.getEvent())));
|
||||||
} else {
|
|
||||||
guardText = escapeDotString(
|
|
||||||
t.getGuard().replaceAll("[\\n\\r]+", " ")
|
|
||||||
.replaceAll("\\s{2,}", " ")
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard
|
||||||
|
if (t.getGuard() != null && !t.getGuard().isBlank()) {
|
||||||
|
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
|
// Actions
|
||||||
String actionsText = "";
|
|
||||||
if (t.getActions() != null && !t.getActions().isEmpty()) {
|
if (t.getActions() != null && !t.getActions().isEmpty()) {
|
||||||
StringBuilder actionBuilder = new StringBuilder();
|
StringBuilder actionBuilder = new StringBuilder();
|
||||||
for (int i = 0; i < t.getActions().size(); i++) {
|
for (int i = 0; i < t.getActions().size(); i++) {
|
||||||
if (i > 0) actionBuilder.append(", ");
|
if (i > 0) actionBuilder.append(", ");
|
||||||
String action = t.getActions().get(i);
|
String action = t.getActions().get(i);
|
||||||
boolean isLambda = i < t.getIsLambdaActions().size() && t.getIsLambdaActions().get(i);
|
boolean isLambda = i < t.getIsLambdaActions().size() && t.getIsLambdaActions().get(i);
|
||||||
if (useLambdaGuards && isLambda) {
|
actionBuilder.append(useLambdaGuards && isLambda ? "λ" : escapeDotString(action));
|
||||||
actionBuilder.append("λ");
|
|
||||||
} else {
|
|
||||||
actionBuilder.append(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(" / ");
|
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(" ");
|
if (!label.isEmpty()) label.append(" ");
|
||||||
label.append("(order=").append(t.getOrder()).append(")");
|
label.append("(order=").append(t.getOrder()).append(")");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build edge attributes
|
// Edge attributes
|
||||||
StringBuilder edgeAttributes = new StringBuilder();
|
StringBuilder edgeAttrs = new StringBuilder();
|
||||||
if (!label.isEmpty()) {
|
if (!label.isEmpty()) {
|
||||||
appendEdgeAttr(edgeAttributes, "label", label.toString());
|
appendEdgeAttr(edgeAttrs, "label", label.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Style/color by type
|
||||||
switch (t.getType()) {
|
switch (t.getType()) {
|
||||||
case "withInternal":
|
case "withInternal":
|
||||||
appendEdgeAttr(edgeAttributes, "style", "dashed");
|
appendEdgeAttr(edgeAttrs, "style", "dashed");
|
||||||
appendEdgeAttr(edgeAttributes, "color", "gray");
|
appendEdgeAttr(edgeAttrs, "color", "gray");
|
||||||
break;
|
break;
|
||||||
case "withLocal":
|
case "withLocal":
|
||||||
appendEdgeAttr(edgeAttributes, "style", "dotted");
|
appendEdgeAttr(edgeAttrs, "style", "dotted");
|
||||||
appendEdgeAttr(edgeAttributes, "color", "darkgray");
|
appendEdgeAttr(edgeAttrs, "color", "darkgray");
|
||||||
break;
|
break;
|
||||||
case "withChoice":
|
case "withChoice":
|
||||||
appendEdgeAttr(edgeAttributes, "style", "solid");
|
String choiceColor = choiceColorMap.getOrDefault(source, "blue");
|
||||||
appendEdgeAttr(edgeAttributes, "color", "blue");
|
appendEdgeAttr(edgeAttrs, "style", "solid");
|
||||||
|
appendEdgeAttr(edgeAttrs, "color", choiceColor);
|
||||||
break;
|
break;
|
||||||
case "withJunction":
|
case "withJunction":
|
||||||
appendEdgeAttr(edgeAttributes, "style", "dotted");
|
appendEdgeAttr(edgeAttrs, "style", "dotted");
|
||||||
appendEdgeAttr(edgeAttributes, "color", "purple");
|
appendEdgeAttr(edgeAttrs, "color", "purple");
|
||||||
break;
|
break;
|
||||||
case "withFork":
|
case "withFork":
|
||||||
appendEdgeAttr(edgeAttributes, "style", "bold");
|
appendEdgeAttr(edgeAttrs, "style", "bold");
|
||||||
appendEdgeAttr(edgeAttributes, "color", "green");
|
appendEdgeAttr(edgeAttrs, "color", "green");
|
||||||
break;
|
break;
|
||||||
case "withJoin":
|
case "withJoin":
|
||||||
appendEdgeAttr(edgeAttributes, "style", "bold");
|
appendEdgeAttr(edgeAttrs, "style", "bold");
|
||||||
appendEdgeAttr(edgeAttributes, "color", "darkorange");
|
appendEdgeAttr(edgeAttrs, "color", "darkorange");
|
||||||
break;
|
break;
|
||||||
case "withExternal":
|
case "withExternal":
|
||||||
default:
|
default:
|
||||||
appendEdgeAttr(edgeAttributes, "style", "solid");
|
appendEdgeAttr(edgeAttrs, "style", "solid");
|
||||||
appendEdgeAttr(edgeAttributes, "color", "black");
|
appendEdgeAttr(edgeAttrs, "color", "black");
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String edgeLabel = !edgeAttributes.isEmpty() ? " [" + edgeAttributes + "]" : "";
|
sb.append(" ").append(edgeSource).append(" -> ").append(target);
|
||||||
|
if (!edgeAttrs.isEmpty()) {
|
||||||
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
|
sb.append(" [").append(edgeAttrs).append("]");
|
||||||
|
}
|
||||||
|
sb.append(";\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +202,7 @@ public class Dot implements StateMachineExporter {
|
|||||||
|
|
||||||
private String escapeDotString(String input) {
|
private String escapeDotString(String input) {
|
||||||
if (input == null) return "";
|
if (input == null) return "";
|
||||||
return input.replace("\\", "\\\\")
|
return input.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
.replace("\"", "\\\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendEdgeAttr(StringBuilder sb, String key, String value) {
|
private void appendEdgeAttr(StringBuilder sb, String key, String value) {
|
||||||
|
|||||||
Reference in New Issue
Block a user