This commit is contained in:
2025-07-05 21:25:53 +02:00
commit 3ad99735ba
17 changed files with 770 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
package com.example.statemachinedemo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
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 java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.Collection;
@SpringBootApplication
public class StatemachinedemoApplication {
public static void main(String[] args) {
SpringApplication.run(StatemachinedemoApplication.class, args);
}
}
enum OrderEvents {
FULFILL, PAY, CANCEL
}
enum OrderStates {
SUBMITTED, PAID, FULFILLED, CANCELED
// ,PAID2, PAID3
}
@Component
@Slf4j
@RequiredArgsConstructor
class Runner implements ApplicationRunner {
private final StateMachineFactory<OrderStates, OrderEvents> factory;
//
// @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");
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 event = transition.getTrigger() != null && transition.getTrigger().getEvent() != null
? transition.getTrigger().getEvent().toString()
: "";
writer.printf("%s --> %s : %s%n", source, target, event);
}
}
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 {
private final StateMachineFactory<OrderStates, OrderEvents> factory;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
StateMachineDotExporter exporter = new StateMachineDotExporter();
try (PrintWriter out = new PrintWriter("statemachine.dot")) {
StateMachineDotExporter.export(stateMachine, out);
}
}
}
@Component
@RequiredArgsConstructor
class Config2 {
private final StateMachineFactory<OrderStates, OrderEvents> factory;
public void a() throws FileNotFoundException {
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
try (PrintWriter out = new PrintWriter("statemachine.uml.dot")) {
StateMachinePlantUMLExporter.export(stateMachine, out);
}
}
}
@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<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)
javax.xml.transform.Transformer transformer = javax.xml.transform.TransformerFactory.newInstance().newTransformer();
javax.xml.transform.stream.StreamResult result = new javax.xml.transform.stream.StreamResult(file);
javax.xml.transform.dom.DOMSource source = new javax.xml.transform.dom.DOMSource(doc);
transformer.transform(source, result);
}
}
@Component
@RequiredArgsConstructor
class Config3 {
private final StateMachineFactory<OrderStates, OrderEvents> factory;
public void exportToSCXML() throws Exception {
StateMachine<OrderStates, OrderEvents> stateMachine = factory.getStateMachine("order");
SCXMLExporter exporter = new SCXMLExporter();
File file = new File("statemachine.scxml");
exporter.exportToSCXML(stateMachine);
}
}
@Configuration
@EnableStateMachineFactory
@Slf4j
class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY)
.and()
.withExternal().source(OrderStates.PAID).target(OrderStates.FULFILLED).event(OrderEvents.FULFILL)
.and()
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELED).event(OrderEvents.CANCEL).guard(new Guard<OrderStates, OrderEvents>() {
@Override
public boolean evaluate(StateContext<OrderStates, OrderEvents> context) {
return false;
}
})
.and()
.withExternal().source(OrderStates.PAID).target(OrderStates.CANCELED).event(OrderEvents.CANCEL);
//
// .and()
// .withChoice()
// .source(OrderStates.SUBMITTED)
// .first(OrderStates.PAID2, new Guard<OrderStates, OrderEvents>() {
// @Override
// public boolean evaluate(StateContext<OrderStates, OrderEvents> context) {
// return false;
// }
// })
// .last(OrderStates.PAID3)
// .and().withExternal().source(OrderStates.PAID2).target(OrderStates.CANCELED)
// .and().withExternal().source(OrderStates.PAID3).target(OrderStates.CANCELED);
}
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states.withStates()
.initial(OrderStates.SUBMITTED)
.state(OrderStates.PAID)
// .state(OrderStates.PAID2)
// .state(OrderStates.PAID3)
.end(OrderStates.FULFILLED)
.end(OrderStates.CANCELED);
}
@Override
public void configure(StateMachineConfigurationConfigurer<OrderStates, OrderEvents> config) throws Exception {
StateMachineListenerAdapter<OrderStates, OrderEvents> listenerAdapter = new StateMachineListenerAdapter<>() {
@Override
public void stateChanged(State<OrderStates, OrderEvents> from, State<OrderStates, OrderEvents> to) {
log.info("state changed from {} to {}", from, to);
}
};
config.withConfiguration()
.autoStartup(false)
.listener(listenerAdapter);
}
}

View File

@@ -0,0 +1 @@
spring.application.name=statemachinedemo

View File

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