package change

This commit is contained in:
2025-07-13 20:11:41 +02:00
parent 4fc75a534e
commit 0cc9af9a08
23 changed files with 248 additions and 289 deletions

View File

@@ -4,7 +4,7 @@ plugins {
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
group = 'click.kamil'
version = '0.0.1-SNAPSHOT'
java {

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo;
package click.kamil.springstatemachineexporter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

View File

@@ -1,16 +1,16 @@
package com.example.statemachinedemo;
package click.kamil.springstatemachineexporter.ast;
import com.example.statemachinedemo.app.AstTransitionParser;
import com.example.statemachinedemo.app.StateMachineStateConfigurationMethodFinder;
import com.example.statemachinedemo.app.StateMachineTransitionConfigurationMethodFinder;
import com.example.statemachinedemo.app.TransitionStateUtils;
import com.example.statemachinedemo.app.domain.Transition;
import com.example.statemachinedemo.common.FileUtils;
import com.example.statemachinedemo.in.AstFileFinder;
import com.example.statemachinedemo.out.Dot;
import com.example.statemachinedemo.out.PlantUml;
import com.example.statemachinedemo.out.Scxml;
import com.example.statemachinedemo.out.StateMachineExporter;
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
import click.kamil.springstatemachineexporter.ast.app.StateMachineStateConfigurationMethodFinder;
import click.kamil.springstatemachineexporter.ast.app.StateMachineTransitionConfigurationMethodFinder;
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.common.FileUtils;
import click.kamil.springstatemachineexporter.ast.in.AstFileFinder;
import click.kamil.springstatemachineexporter.ast.out.Dot;
import click.kamil.springstatemachineexporter.ast.out.PlantUml;
import click.kamil.springstatemachineexporter.ast.out.Scxml;
import click.kamil.springstatemachineexporter.ast.out.StateMachineExporter;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;

View File

@@ -1,6 +1,6 @@
package com.example.statemachinedemo.app;
package click.kamil.springstatemachineexporter.ast.app;
import com.example.statemachinedemo.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.app;
package click.kamil.springstatemachineexporter.ast.app;
import lombok.Getter;
import org.eclipse.jdt.core.dom.*;

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.app;
package click.kamil.springstatemachineexporter.ast.app;
import lombok.Getter;
import org.eclipse.jdt.core.dom.*;

View File

@@ -1,6 +1,6 @@
package com.example.statemachinedemo.app;
package click.kamil.springstatemachineexporter.ast.app;
import com.example.statemachinedemo.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.Collection;
import java.util.Objects;

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.app.domain;
package click.kamil.springstatemachineexporter.ast.app.domain;
import lombok.Getter;
import lombok.Setter;

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.common;
package click.kamil.springstatemachineexporter.ast.common;
import java.io.IOException;
import java.nio.file.Files;

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.in;
package click.kamil.springstatemachineexporter.ast.in;
import org.eclipse.jdt.core.dom.*;

View File

@@ -1,6 +1,6 @@
package com.example.statemachinedemo.out;
package click.kamil.springstatemachineexporter.ast.out;
import com.example.statemachinedemo.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.HashSet;
import java.util.List;

View File

@@ -1,6 +1,6 @@
package com.example.statemachinedemo.out;
package click.kamil.springstatemachineexporter.ast.out;
import com.example.statemachinedemo.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import io.micrometer.common.util.StringUtils;
import java.util.*;
@@ -8,6 +8,20 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class PlantUml implements StateMachineExporter {
// Color palette for choices
private final List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
private String getColor(Transition t, String source, Map<String, String> choiceStateColorMap) {
return switch (t.getType()) {
case "withChoice" -> choiceStateColorMap.getOrDefault(source, "000000");
case "withExternal" -> "1E90FF";
case "withInternal" -> "32CD32";
case "withLocal" -> "FFA500";
case "withJunction" -> "FF69B4";
case "withJoin" -> "8A2BE2";
case "withFork" -> "20B2AA";
default -> "000000";
};
}
@Override
public String export(List<Transition> transitions,
@@ -44,9 +58,6 @@ public class PlantUml implements StateMachineExporter {
}
sb.append("\n");
// Color palette for choices
List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
// Assign color to each choice state
Map<String, String> choiceStateColorMap = new HashMap<>();
int colorIndex = 0;
@@ -168,17 +179,4 @@ public class PlantUml implements StateMachineExporter {
return String.join(" ", parts);
}
private String getColor(Transition t, String source, Map<String, String> choiceStateColorMap) {
return switch (t.getType()) {
case "withChoice" -> choiceStateColorMap.getOrDefault(source, "000000");
case "withExternal" -> "1E90FF";
case "withInternal" -> "32CD32";
case "withLocal" -> "FFA500";
case "withJunction" -> "FF69B4";
case "withJoin" -> "8A2BE2";
case "withFork" -> "20B2AA";
default -> "000000";
};
}
}

View File

@@ -1,6 +1,6 @@
package com.example.statemachinedemo.out;
package click.kamil.springstatemachineexporter.ast.out;
import com.example.statemachinedemo.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.HashSet;
import java.util.List;

View File

@@ -1,6 +1,6 @@
package com.example.statemachinedemo.out;
package click.kamil.springstatemachineexporter.ast.out;
import com.example.statemachinedemo.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import java.util.List;
import java.util.Set;

View File

@@ -0,0 +1,180 @@
package click.kamil.springstatemachineexporter.springbased;
import click.kamil.springstatemachineexporter.statemachine.OrderEvents;
import click.kamil.springstatemachineexporter.statemachine.OrderStates;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.PrintWriter;
public class SpringExporter {
}
@Component
@Slf4j
@RequiredArgsConstructor
class Runner implements ApplicationRunner {
private final DotExporter dotExporter;
private final PlantUmlExporter plantUmlExporter;
private final ScxmlExporter scxmlExporter;
@Override
public void run(ApplicationArguments args) throws Exception {
dotExporter.export("stateMachine.dot");
plantUmlExporter.export("stateMachine.uml.dot");
scxmlExporter.export("stateMachine.scxml.xml");
}
}
abstract class BaseExporter {
@Qualifier("simpleEnumStateMachineFactory")
protected final StateMachineFactory<OrderStates, OrderEvents> factory;
protected BaseExporter(StateMachineFactory<OrderStates, OrderEvents> factory) {
this.factory = factory;
}
abstract void export(String name) throws Exception;
protected StateMachine<OrderStates, OrderEvents> createStateMachine() {
return factory.getStateMachine("order");
}
}
@Component
class DotExporter extends BaseExporter {
public DotExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
super(factory);
}
@Override
public void export(String filename) throws Exception {
try (PrintWriter writer = new PrintWriter(filename)) {
StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
writer.println("digraph stateMachine {");
for (State<OrderStates, OrderEvents> state : machine.getStates()) {
writer.printf(" %s;%n", state.getId());
}
for (Transition<OrderStates, OrderEvents> transition : machine.getTransitions()) {
if (transition.getSource() != null && transition.getTarget() != null) {
writer.printf(" %s -> %s [label=\"%s\"];%n",
transition.getSource().getId(),
transition.getTarget().getId(),
transition.getTrigger() != null ? transition.getTrigger().getEvent() : "");
}
}
writer.println("}");
}
}
}
@Component
class PlantUmlExporter extends BaseExporter {
public PlantUmlExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
super(factory);
}
@Override
public void export(String filename) throws Exception {
try (PrintWriter writer = new PrintWriter(filename)) {
StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
writer.println("@startuml");
writer.println("[*] --> " + machine.getInitialState().getId());
for (Transition<OrderStates, OrderEvents> transition : machine.getTransitions()) {
if (transition.getSource() != null && transition.getTarget() != null) {
String source = transition.getSource().getId().toString();
String target = transition.getTarget().getId().toString();
String label = "";
if (transition.getTrigger() != null && transition.getTrigger().getEvent() != null) {
label += transition.getTrigger().getEvent().toString();
}
if (transition.getGuard() != null) {
if (!label.isEmpty()) label += " ";
label += "[guard]";
}
writer.printf("%s --> %s%s%n", source, target, !label.isEmpty() ? " : " + label : "");
}
}
for (State<OrderStates, OrderEvents> state : machine.getStates()) {
if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.END) {
writer.printf("%s --> [*]%n", state.getId());
}
}
writer.println("@enduml");
}
}
}
@Component
class ScxmlExporter extends BaseExporter {
public ScxmlExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
super(factory);
}
@Override
public void export(String filename) throws Exception {
StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.newDocument();
Element scxml = doc.createElement("scxml");
scxml.setAttribute("version", "1.0");
doc.appendChild(scxml);
Element statesElement = doc.createElement("states");
scxml.appendChild(statesElement);
for (State<OrderStates, OrderEvents> state : machine.getStates()) {
Element stateEl = doc.createElement("state");
stateEl.setAttribute("id", state.getId().toString());
statesElement.appendChild(stateEl);
}
Element transitionsElement = doc.createElement("transitions");
scxml.appendChild(transitionsElement);
for (Transition<OrderStates, OrderEvents> transition : machine.getTransitions()) {
Element t = doc.createElement("transition");
t.setAttribute("from", transition.getSource().getId().toString());
t.setAttribute("to", transition.getTarget().getId().toString());
t.setAttribute("event", String.valueOf(transition.getTrigger()));
transitionsElement.appendChild(t);
}
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(new File(filename)));
}
}

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.z.statemachine;
package click.kamil.springstatemachineexporter.statemachine;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.action.Action;

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.z.statemachine;
package click.kamil.springstatemachineexporter.statemachine;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.action.Action;

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.z.statemachine;
package click.kamil.springstatemachineexporter.statemachine;
public enum OrderEvents {
FULFILL, PAY, CANCEL,

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.z.statemachine;
package click.kamil.springstatemachineexporter.statemachine;
public enum OrderStates {
SUBMITTED, PAID, FULFILLED, CANCELED

View File

@@ -1,4 +1,4 @@
package com.example.statemachinedemo.z.statemachine;
package click.kamil.springstatemachineexporter.statemachine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

View File

@@ -1,224 +0,0 @@
package com.example.statemachinedemo.z.notgoodspringbased;
import com.example.statemachinedemo.z.statemachine.OrderEvents;
import com.example.statemachinedemo.z.statemachine.OrderStates;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.Collection;
public class SpringExporter {
}
@Component
@Slf4j
@RequiredArgsConstructor
class Runner implements ApplicationRunner {
// private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
// @Override
// public void run(ApplicationArguments args) throws Exception {
// StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
// stateMachine.start();
// log.info("Current state is {}", stateMachine.getState().getId());
//
// }
private final Config1 c;
private final Config2 c2;
private final Config3 c3;
@Override
public void run(ApplicationArguments args) throws Exception {
c.a();
c2.a();
c3.exportToSCXML();
}
}
class StateMachineDotExporter {
public static <S, E> void export(StateMachine<S, E> stateMachine, PrintWriter writer) {
writer.println("digraph stateMachine {");
for (State<S, E> state : stateMachine.getStates()) {
writer.printf(" %s;%n", state.getId());
}
for (Transition<S, E> transition : stateMachine.getTransitions()) {
if (transition.getSource() != null && transition.getTarget() != null) {
writer.printf(" %s -> %s [label=\"%s\"];%n",
transition.getSource().getId(),
transition.getTarget().getId(),
transition.getTrigger() != null ? transition.getTrigger().getEvent() : "");
}
}
writer.println("}");
}
}
class StateMachinePlantUMLExporter {
public static <S, E> void export(StateMachine<S, E> stateMachine, PrintWriter writer) {
writer.println("@startuml");
// Initial state
writer.println("[*] --> " + stateMachine.getInitialState().getId());
for (Transition<S, E> transition : stateMachine.getTransitions()) {
if (transition.getSource() != null && transition.getTarget() != null) {
String source = transition.getSource().getId().toString();
String target = transition.getTarget().getId().toString();
String label = "";
if (transition.getTrigger() != null && transition.getTrigger().getEvent() != null) {
label += transition.getTrigger().getEvent().toString();
}
if (transition.getGuard() != null) {
// If event is empty but theres a guard, still label it meaningfully
if (!label.isEmpty()) {
label += " ";
}
label += "[guard]";
}
// If there's no label at all, don't include ":"
if (!label.isEmpty()) {
writer.printf("%s --> %s : %s%n", source, target, label);
} else {
writer.printf("%s --> %s%n", source, target);
}
}
}
// End states
for (State<S, E> state : stateMachine.getStates()) {
if (state.getPseudoState() != null &&
state.getPseudoState().getKind() == PseudoStateKind.END) {
writer.printf("%s --> [*]%n", state.getId());
}
}
writer.println("@enduml");
}
}
@Component
@RequiredArgsConstructor
class Config1 {
@Qualifier("simpleEnumStateMachineFactory")
private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order");
StateMachineDotExporter exporter = new StateMachineDotExporter();
try (PrintWriter out = new PrintWriter("statemachine.dot")) {
StateMachineDotExporter.export(stateMachine, out);
}
}
}
@Component
@RequiredArgsConstructor
class Config2 {
@Qualifier("simpleEnumStateMachineFactory")
private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order");
try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) {
StateMachinePlantUMLExporter.export(stateMachine, out);
}
}
}
@Component
@RequiredArgsConstructor
class Config3 {
@Qualifier("simpleEnumStateMachineFactory")
private final StateMachineFactory<OrderStates, OrderEvents> simpleEnumStateMachineConfiguration;
public void exportToSCXML() throws Exception {
StateMachine<OrderStates, OrderEvents> stateMachine = simpleEnumStateMachineConfiguration.getStateMachine("order");
SCXMLExporter exporter = new SCXMLExporter();
File file = new File("statemachine.scxml.xml");
exporter.exportToSCXML(stateMachine);
}
}
@Component
class SCXMLExporter {
// This method assumes you already have a state machine
public <S, E> void exportToSCXML(StateMachine<S, E> stateMachine) throws Exception {
// Create a document to represent the SCXML structure
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
// Root SCXML element
Element scxmlElement = doc.createElement("scxml");
scxmlElement.setAttribute("version", "1.0");
doc.appendChild(scxmlElement);
// Define the states
Element statesElement = doc.createElement("states");
scxmlElement.appendChild(statesElement);
// Iterate over the states of the state machine
Collection<State<S, E>> states = stateMachine.getStates();
for (State<S, E> state : states) {
Element stateElement = doc.createElement("state");
stateElement.setAttribute("id", state.getId().toString());
statesElement.appendChild(stateElement);
}
// Define the transitions
Element transitionsElement = doc.createElement("transitions");
scxmlElement.appendChild(transitionsElement);
Collection<org.springframework.statemachine.transition.Transition<S, E>> transitions = stateMachine.getTransitions();
for (Transition<S, E> transition : transitions) {
Element transitionElement = doc.createElement("transition");
transitionElement.setAttribute("from", transition.getSource().getId().toString());
transitionElement.setAttribute("to", transition.getTarget().getId().toString());
transitionElement.setAttribute("event", String.valueOf(transition.getTrigger()));
transitionsElement.appendChild(transitionElement);
}
// Write to file
File file = new File("stateMachine.scxml");
saveDocumentToFile(doc, file);
}
private void saveDocumentToFile(Document doc, File file) throws Exception {
// Serialize the document to a file (you can use your preferred XML writer)
Transformer transformer = TransformerFactory.newInstance().newTransformer();
StreamResult result = new StreamResult(file);
DOMSource source = new DOMSource(doc);
transformer.transform(source, result);
}
}

View File

@@ -0,0 +1,18 @@
package click.kamil.springstatemachineexporter;
import org.junit.jupiter.api.Test;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@SpringBootTest
class StatemachinedemoApplicationTests {
@MockitoBean
CommandLineRunner commandLineRunner;
@Test
void contextLoads() {
}
}

View File

@@ -1,13 +0,0 @@
package com.example.statemachinedemo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class StatemachinedemoApplicationTests {
@Test
void contextLoads() {
}
}