forks work
This commit is contained in:
@@ -3,148 +3,172 @@ package com.example.statemachinedemo;
|
||||
import java.util.*;
|
||||
|
||||
public class ExportUtils {
|
||||
|
||||
public static String generatePlantUML(List<Transition> transitions,
|
||||
Set<String> startStates,
|
||||
Set<String> endStates,
|
||||
boolean useLambdaGuards) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("@startuml\n");
|
||||
sb.append("skinparam state {\n");
|
||||
sb.append(" BackgroundColor<<choice>> LightYellow\n");
|
||||
sb.append("}\n\n");
|
||||
Set<String> startStates,
|
||||
Set<String> endStates,
|
||||
boolean useLambdaGuards) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("@startuml\n");
|
||||
sb.append("skinparam state {\n");
|
||||
sb.append(" BackgroundColor<<choice>> LightYellow\n");
|
||||
sb.append("}\n\n");
|
||||
|
||||
// 1. Print start states
|
||||
for (String start : startStates) {
|
||||
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
// 1. Print start states
|
||||
for (String start : startStates) {
|
||||
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// 2. Find all states that have choice transitions originating from them
|
||||
Set<String> statesWithChoice = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
if ("withChoice".equals(t.type)) {
|
||||
statesWithChoice.add(simplify(t.sourceState));
|
||||
// 2. Find all states that have choice transitions originating from them
|
||||
Set<String> statesWithChoice = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
if ("withChoice".equals(t.type)) {
|
||||
for (String source : t.sourceStates) {
|
||||
statesWithChoice.add(simplify(source));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Declare choice pseudostates & connect from original state
|
||||
for (String state : statesWithChoice) {
|
||||
String choiceState = state + "_choice";
|
||||
sb.append("state ").append(choiceState).append(" <<choice>>\n");
|
||||
sb.append(state).append(" --> ").append(choiceState).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
// 3. Declare choice pseudostates & connect from original state
|
||||
for (String state : statesWithChoice) {
|
||||
String choiceState = state + "_choice";
|
||||
sb.append("state ").append(choiceState).append(" <<choice>>\n");
|
||||
sb.append(state).append(" --> ").append(choiceState).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// Palette of colors for choice states (NO leading '#')
|
||||
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
||||
// Palette of colors for choice states (NO leading '#')
|
||||
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
||||
|
||||
// Assign a color per withChoice source state cycling through palette
|
||||
Map<String, String> choiceStateColorMap = new HashMap<>();
|
||||
int colorIndex = 0;
|
||||
for (String state : statesWithChoice) {
|
||||
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
|
||||
colorIndex++;
|
||||
}
|
||||
// Assign a color per withChoice source state cycling through palette
|
||||
Map<String, String> choiceStateColorMap = new HashMap<>();
|
||||
int colorIndex = 0;
|
||||
for (String state : statesWithChoice) {
|
||||
choiceStateColorMap.put(state, choiceColors.get(colorIndex % choiceColors.size()));
|
||||
colorIndex++;
|
||||
}
|
||||
|
||||
Set<String> transitionLines = new HashSet<>();
|
||||
Set<String> transitionLines = new HashSet<>();
|
||||
|
||||
// Helper to get color for non-choice transitions (NO leading '#')
|
||||
java.util.function.Function<String, String> getTransitionColor = (type) -> {
|
||||
if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue
|
||||
if ("withInternal".equals(type)) return "32CD32"; // LimeGreen
|
||||
if ("withJunction".equals(type)) return "FF69B4"; // HotPink
|
||||
return "000000"; // Black fallback
|
||||
};
|
||||
// Helper to get color for non-choice transitions (NO leading '#')
|
||||
java.util.function.Function<String, String> getTransitionColor = (type) -> {
|
||||
if ("withExternal".equals(type)) return "1E90FF"; // DodgerBlue
|
||||
if ("withInternal".equals(type)) return "32CD32"; // LimeGreen
|
||||
if ("withJunction".equals(type)) return "FF69B4"; // HotPink
|
||||
return "000000"; // Black fallback
|
||||
};
|
||||
|
||||
// 4. Output transitions
|
||||
for (Transition t : transitions) {
|
||||
if (t.sourceState == null || t.targetState == null) continue;
|
||||
// 4. Output transitions
|
||||
for (Transition t : transitions) {
|
||||
if (t.sourceStates == null || t.sourceStates.isEmpty()) continue;
|
||||
|
||||
String source = simplify(t.sourceState);
|
||||
String target = simplify(t.targetState);
|
||||
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
||||
|
||||
String transitionSource = source;
|
||||
|
||||
// Reroute choice transitions through pseudostate
|
||||
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
||||
transitionSource = source + "_choice";
|
||||
List<String> targets;
|
||||
if (t.targetStates == null || t.targetStates.isEmpty()) {
|
||||
if (!allowNoTarget) {
|
||||
// Use sourceStates as targets (self-loop)
|
||||
targets = t.sourceStates;
|
||||
} else {
|
||||
// withJoin or withFork with no targets => skip emitting transitions
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
targets = t.targetStates;
|
||||
}
|
||||
|
||||
// Prepare guard text
|
||||
String guardText = "";
|
||||
if (t.guard != null && !t.guard.isBlank()) {
|
||||
if (useLambdaGuards && t.isLambdaGuard) {
|
||||
guardText = "lambda";
|
||||
} else {
|
||||
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||
for (String rawSource : t.sourceStates) {
|
||||
String source = simplify(rawSource);
|
||||
|
||||
for (String rawTarget : targets) {
|
||||
String target = simplify(rawTarget);
|
||||
|
||||
String transitionSource = source;
|
||||
|
||||
// Reroute choice transitions through pseudostate
|
||||
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
||||
transitionSource = source + "_choice";
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare actions text (comma separated)
|
||||
String actionsText = "";
|
||||
if (t.actions != null && !t.actions.isEmpty()) {
|
||||
StringBuilder actionBuilder = new StringBuilder();
|
||||
for (int i = 0; i < t.actions.size(); i++) {
|
||||
if (i > 0) actionBuilder.append(", ");
|
||||
String action = t.actions.get(i);
|
||||
boolean isLambda = t.isLambdaActions.get(i);
|
||||
if (useLambdaGuards && isLambda) {
|
||||
actionBuilder.append("lambda");
|
||||
// Prepare guard text
|
||||
String guardText = "";
|
||||
if (t.guard != null && !t.guard.isBlank()) {
|
||||
if (useLambdaGuards && t.isLambdaGuard) {
|
||||
guardText = "lambda";
|
||||
} else {
|
||||
actionBuilder.append(action);
|
||||
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||
}
|
||||
}
|
||||
actionsText = actionBuilder.toString();
|
||||
}
|
||||
|
||||
// Build label with event, guard, and actions
|
||||
StringBuilder label = new StringBuilder();
|
||||
if (t.event != null && !t.event.isBlank()) {
|
||||
label.append(simplify(t.event));
|
||||
}
|
||||
if (!guardText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("[").append(guardText).append("]");
|
||||
}
|
||||
if (!actionsText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" / ");
|
||||
label.append(actionsText);
|
||||
}
|
||||
// Prepare actions text (comma separated)
|
||||
String actionsText = "";
|
||||
if (t.actions != null && !t.actions.isEmpty()) {
|
||||
StringBuilder actionBuilder = new StringBuilder();
|
||||
for (int i = 0; i < t.actions.size(); i++) {
|
||||
if (i > 0) actionBuilder.append(", ");
|
||||
String action = t.actions.get(i);
|
||||
boolean isLambda = t.isLambdaActions.get(i);
|
||||
if (useLambdaGuards && isLambda) {
|
||||
actionBuilder.append("lambda");
|
||||
} else {
|
||||
actionBuilder.append(action);
|
||||
}
|
||||
}
|
||||
actionsText = actionBuilder.toString();
|
||||
}
|
||||
|
||||
// Add order info for choice/junction transitions
|
||||
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("(order=").append(t.order).append(")");
|
||||
}
|
||||
// Build label with event, guard, and actions
|
||||
StringBuilder label = new StringBuilder();
|
||||
if (t.event != null && !t.event.isBlank()) {
|
||||
label.append(simplify(t.event));
|
||||
}
|
||||
if (!guardText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("[").append(guardText).append("]");
|
||||
}
|
||||
if (!actionsText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" / ");
|
||||
label.append(actionsText);
|
||||
}
|
||||
|
||||
// Determine color for this transition
|
||||
String color;
|
||||
if ("withChoice".equals(t.type)) {
|
||||
color = choiceStateColorMap.getOrDefault(source, "000000");
|
||||
} else {
|
||||
color = getTransitionColor.apply(t.type);
|
||||
}
|
||||
// Add order info for choice/junction transitions
|
||||
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("(order=").append(t.order).append(")");
|
||||
}
|
||||
|
||||
// Add color with single '#' prefix
|
||||
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
|
||||
// Determine color for this transition
|
||||
String color;
|
||||
if ("withChoice".equals(t.type)) {
|
||||
color = choiceStateColorMap.getOrDefault(source, "000000");
|
||||
} else {
|
||||
color = getTransitionColor.apply(t.type);
|
||||
}
|
||||
|
||||
// Skip duplicates
|
||||
if (transitionLines.add(line)) {
|
||||
sb.append(line).append("\n");
|
||||
// Add color with single '#' prefix
|
||||
String line = transitionSource + " -[#"+color+",bold]-> " + target + (label.isEmpty() ? "" : (" : " + label));
|
||||
|
||||
// Skip duplicates
|
||||
if (transitionLines.add(line)) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
|
||||
// 5. Mark end states with transition to [*]
|
||||
for (String end : endStates) {
|
||||
sb.append(simplify(end)).append(" --> [*]\n");
|
||||
}
|
||||
|
||||
sb.append("@enduml\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
|
||||
// 5. Mark end states with transition to [*]
|
||||
for (String end : endStates) {
|
||||
sb.append(simplify(end)).append(" --> [*]\n");
|
||||
}
|
||||
|
||||
sb.append("@enduml\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
public static String generateDot(List<Transition> transitions,
|
||||
Set<String> startStates,
|
||||
Set<String> endStates,
|
||||
@@ -161,15 +185,21 @@ public class ExportUtils {
|
||||
Set<String> statesWithChoice = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
if ("withChoice".equals(t.type)) {
|
||||
statesWithChoice.add(simplify(t.sourceState));
|
||||
for (String source : t.sourceStates) {
|
||||
statesWithChoice.add(simplify(source));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Declare normal states and choice pseudostates
|
||||
Set<String> allStates = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
allStates.add(simplify(t.sourceState));
|
||||
allStates.add(simplify(t.targetState));
|
||||
if (t.sourceStates != null) {
|
||||
for (String s : t.sourceStates) allStates.add(simplify(s));
|
||||
}
|
||||
if (t.targetStates != null) {
|
||||
for (String tgt : t.targetStates) allStates.add(simplify(tgt));
|
||||
}
|
||||
}
|
||||
|
||||
// Print all normal states (except choice pseudostates for now)
|
||||
@@ -205,66 +235,86 @@ public class ExportUtils {
|
||||
|
||||
// 6. Transitions: reroute withChoice transitions via choice pseudostate
|
||||
for (Transition t : transitions) {
|
||||
if (t.sourceState == null || t.targetState == null) continue;
|
||||
if (t.sourceStates == null || t.sourceStates.isEmpty()) continue;
|
||||
|
||||
String source = simplify(t.sourceState);
|
||||
String target = simplify(t.targetState);
|
||||
String edgeSource = source;
|
||||
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
||||
|
||||
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
||||
edgeSource = source + "_choice";
|
||||
}
|
||||
|
||||
// Prepare guard text
|
||||
String guardText = "";
|
||||
if (t.guard != null && !t.guard.isBlank()) {
|
||||
if (useLambdaGuards && t.isLambdaGuard) {
|
||||
guardText = "lambda";
|
||||
List<String> targets;
|
||||
if (t.targetStates == null || t.targetStates.isEmpty()) {
|
||||
if (!allowNoTarget) {
|
||||
// self-loop: targets = sources
|
||||
targets = t.sourceStates;
|
||||
} else {
|
||||
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||
// withJoin/withFork with no targets: skip
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
targets = t.targetStates;
|
||||
}
|
||||
|
||||
// Prepare actions text
|
||||
String actionsText = "";
|
||||
if (t.actions != null && !t.actions.isEmpty()) {
|
||||
StringBuilder actionBuilder = new StringBuilder();
|
||||
for (int i = 0; i < t.actions.size(); i++) {
|
||||
if (i > 0) actionBuilder.append(", ");
|
||||
String action = t.actions.get(i);
|
||||
boolean isLambda = t.isLambdaActions.get(i);
|
||||
if (useLambdaGuards && isLambda) {
|
||||
actionBuilder.append("lambda");
|
||||
} else {
|
||||
actionBuilder.append(action);
|
||||
for (String rawSource : t.sourceStates) {
|
||||
String source = simplify(rawSource);
|
||||
|
||||
for (String rawTarget : targets) {
|
||||
String target = simplify(rawTarget);
|
||||
String edgeSource = source;
|
||||
|
||||
if ("withChoice".equals(t.type) && statesWithChoice.contains(source)) {
|
||||
edgeSource = source + "_choice";
|
||||
}
|
||||
|
||||
// Prepare guard text
|
||||
String guardText = "";
|
||||
if (t.guard != null && !t.guard.isBlank()) {
|
||||
if (useLambdaGuards && t.isLambdaGuard) {
|
||||
guardText = "lambda";
|
||||
} else {
|
||||
guardText = t.guard.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare actions text
|
||||
String actionsText = "";
|
||||
if (t.actions != null && !t.actions.isEmpty()) {
|
||||
StringBuilder actionBuilder = new StringBuilder();
|
||||
for (int i = 0; i < t.actions.size(); i++) {
|
||||
if (i > 0) actionBuilder.append(", ");
|
||||
String action = t.actions.get(i);
|
||||
boolean isLambda = t.isLambdaActions.get(i);
|
||||
if (useLambdaGuards && isLambda) {
|
||||
actionBuilder.append("lambda");
|
||||
} else {
|
||||
actionBuilder.append(action);
|
||||
}
|
||||
}
|
||||
actionsText = actionBuilder.toString();
|
||||
}
|
||||
|
||||
// Build label with event, guard, and actions
|
||||
StringBuilder label = new StringBuilder();
|
||||
if (t.event != null && !t.event.isBlank()) {
|
||||
label.append(simplify(t.event));
|
||||
}
|
||||
if (!guardText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("[").append(guardText).append("]");
|
||||
}
|
||||
if (!actionsText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" / ");
|
||||
label.append(actionsText);
|
||||
}
|
||||
|
||||
// Add order info for choice/junction transitions
|
||||
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("(order=").append(t.order).append(")");
|
||||
}
|
||||
|
||||
String edgeLabel = label.length() > 0 ? " [label=\"" + label.toString() + "\"]" : "";
|
||||
|
||||
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
|
||||
}
|
||||
actionsText = actionBuilder.toString();
|
||||
}
|
||||
|
||||
// Build label with event, guard, and actions
|
||||
StringBuilder label = new StringBuilder();
|
||||
if (t.event != null && !t.event.isBlank()) {
|
||||
label.append(simplify(t.event));
|
||||
}
|
||||
if (!guardText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("[").append(guardText).append("]");
|
||||
}
|
||||
if (!actionsText.isBlank()) {
|
||||
if (label.length() > 0) label.append(" / ");
|
||||
label.append(actionsText);
|
||||
}
|
||||
|
||||
// Add order info for choice/junction transitions
|
||||
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
||||
if (label.length() > 0) label.append(" ");
|
||||
label.append("(order=").append(t.order).append(")");
|
||||
}
|
||||
|
||||
String edgeLabel = label.length() > 0 ? " [label=\"" + label.toString() + "\"]" : "";
|
||||
|
||||
sb.append(" ").append(edgeSource).append(" -> ").append(target).append(edgeLabel).append(";\n");
|
||||
}
|
||||
|
||||
sb.append("}\n");
|
||||
@@ -282,8 +332,12 @@ public class ExportUtils {
|
||||
// 1. Collect all states
|
||||
Set<String> allStates = new HashSet<>();
|
||||
for (Transition t : transitions) {
|
||||
if (t.sourceState != null) allStates.add(t.sourceState);
|
||||
if (t.targetState != null) allStates.add(t.targetState);
|
||||
if (t.sourceStates != null) {
|
||||
for (String s : t.sourceStates) allStates.add(s);
|
||||
}
|
||||
if (t.targetStates != null) {
|
||||
for (String tState : t.targetStates) allStates.add(tState);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Output <state> elements with transitions
|
||||
@@ -300,8 +354,25 @@ public class ExportUtils {
|
||||
|
||||
// Add transitions from this state
|
||||
for (Transition t : transitions) {
|
||||
if (state.equals(t.sourceState)) {
|
||||
String target = simplify(t.targetState);
|
||||
if (t.sourceStates == null || !t.sourceStates.contains(state)) continue;
|
||||
|
||||
boolean allowNoTarget = "withJoin".equals(t.type) || "withFork".equals(t.type);
|
||||
|
||||
List<String> targets;
|
||||
if (t.targetStates == null || t.targetStates.isEmpty()) {
|
||||
if (!allowNoTarget) {
|
||||
// self-loop target = source
|
||||
targets = List.of(state);
|
||||
} else {
|
||||
// withJoin/withFork with no target: skip
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
targets = t.targetStates;
|
||||
}
|
||||
|
||||
for (String rawTarget : targets) {
|
||||
String target = simplify(rawTarget);
|
||||
if (target == null || target.isEmpty()) continue;
|
||||
|
||||
String event = t.event != null ? simplify(t.event) : null;
|
||||
@@ -319,7 +390,7 @@ public class ExportUtils {
|
||||
sb.append(" target=\"").append(target).append("\"");
|
||||
if (guard != null && !guard.isBlank()) sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
|
||||
|
||||
// If choice/junction and order is set, add XML comment inside transition
|
||||
// Add order info for choice/junction transitions as XML comment inside transition
|
||||
if (("withChoice".equals(t.type) || "withJunction".equals(t.type)) && t.order != null) {
|
||||
sb.append(">\n");
|
||||
sb.append(" <!-- order=").append(t.order).append(" -->\n");
|
||||
|
||||
Reference in New Issue
Block a user