diff --git a/run_golden.gradle b/run_golden.gradle index 6363a67..41f1735 100644 --- a/run_golden.gradle +++ b/run_golden.gradle @@ -1,4 +1,5 @@ task runGolden(type: JavaExec) { mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater" classpath = sourceSets.test.runtimeClasspath + workingDir = rootProject.projectDir } diff --git a/settings.gradle b/settings.gradle index 7bf430a..212252e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,3 +19,4 @@ include ':state_machines:ultimate_ecosystem_sm' include ':state_machines:enterprise_order_system' include ':state_machines:multi_module_sample:api-module' include ':state_machines:multi_module_sample:core-module' +include ':state_machines:state_machine_enterprise' diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java index f789b06..2cb3a9b 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/GoldenUpdater.java @@ -158,6 +158,24 @@ public class GoldenUpdater { Path.of("state_machines/inheritance_extra_functions3_state_machine"), Path.of("src/test/resources/golden/G2StateMachineConfiguration"), "G2StateMachineConfiguration" + ), + new TestScenario( + "Enterprise Order State Machine", + Path.of("state_machines/state_machine_enterprise"), + Path.of("src/test/resources/golden/OrderStateMachineConfiguration"), + "OrderStateMachineConfiguration" + ), + new TestScenario( + "Enterprise Document State Machine", + Path.of("state_machines/state_machine_enterprise"), + Path.of("src/test/resources/golden/DocumentStateMachineConfiguration"), + "DocumentStateMachineConfiguration" + ), + new TestScenario( + "Enterprise User State Machine", + Path.of("state_machines/state_machine_enterprise"), + Path.of("src/test/resources/golden/UserStateMachineConfiguration"), + "UserStateMachineConfiguration" ) ); } diff --git a/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.dot b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.dot new file mode 100644 index 0000000..17ae9cf --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.dot @@ -0,0 +1,13 @@ +digraph statemachine { + rankdir=LR; + node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; + edge [fontname="Arial", fontsize=10]; + + _start [shape=circle, label="", fillcolor=black, width=0.1]; + _start -> DRAFT; + APPROVED [fillcolor=lightgray]; + DRAFT -> IN_REVIEW [label="DocumentEvent.SUBMIT", style="solid", color="black"]; + IN_REVIEW -> APPROVED [label="DocumentEvent.APPROVE", style="solid", color="black"]; + IN_REVIEW -> DRAFT [label="DocumentEvent.REJECT", style="solid", color="black"]; +} + diff --git a/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.json new file mode 100644 index 0000000..cdf9499 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.json @@ -0,0 +1,89 @@ +{ + "metadata" : { + "triggers" : [ ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/machine/{machineType}/transition/{event}", + "className" : "click.kamil.enterprise.web.StateMachineController", + "methodName" : "transition", + "sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java", + "metadata" : { + "path" : "/api/machine/{machineType}/transition/{event}", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "machineType", + "type" : "String", + "annotations" : [ "PathVariable" ] + }, { + "name" : "event", + "type" : "String", + "annotations" : [ "PathVariable" ] + }, { + "name" : "userId", + "type" : "String", + "annotations" : [ "RequestParam" ] + } ] + } ], + "callChains" : [ ], + "properties" : { + "default" : { } + } + }, + "name" : "click.kamil.enterprise.machines.document.DocumentStateMachineConfiguration", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "DocumentState.DRAFT" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "DocumentState.DRAFT", + "fullIdentifier" : "DocumentState.DRAFT" + } ], + "targetStates" : [ { + "rawName" : "DocumentState.IN_REVIEW", + "fullIdentifier" : "DocumentState.IN_REVIEW" + } ], + "event" : { + "rawName" : "DocumentEvent.SUBMIT", + "fullIdentifier" : "DocumentEvent.SUBMIT" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "DocumentState.IN_REVIEW", + "fullIdentifier" : "DocumentState.IN_REVIEW" + } ], + "targetStates" : [ { + "rawName" : "DocumentState.APPROVED", + "fullIdentifier" : "DocumentState.APPROVED" + } ], + "event" : { + "rawName" : "DocumentEvent.APPROVE", + "fullIdentifier" : "DocumentEvent.APPROVE" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "DocumentState.IN_REVIEW", + "fullIdentifier" : "DocumentState.IN_REVIEW" + } ], + "targetStates" : [ { + "rawName" : "DocumentState.DRAFT", + "fullIdentifier" : "DocumentState.DRAFT" + } ], + "event" : { + "rawName" : "DocumentEvent.REJECT", + "fullIdentifier" : "DocumentEvent.REJECT" + }, + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "DocumentState.APPROVED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.png b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.png new file mode 100644 index 0000000..f6db9f2 Binary files /dev/null and b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.png differ diff --git a/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.puml new file mode 100644 index 0000000..f93ba9d --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.puml @@ -0,0 +1,33 @@ +@startuml +!pragma layout smetana +set separator none +hide empty description +hide stereotype +skinparam state { + BackgroundColor white + BorderColor #94a3b8 + BorderThickness 1 + FontName Inter + FontSize 9 + FontStyle bold + RoundCorner 20 + Padding 1 +} +skinparam shadowing false +skinparam ArrowFontName JetBrains Mono +skinparam ArrowFontSize 8 +skinparam ArrowColor #cbd5e1 +skinparam ArrowThickness 1 +skinparam dpi 110 +skinparam svgLinkTarget _self + +[*] --> DocumentState.DRAFT + + +DocumentState.DRAFT -[#1E90FF,bold]-> DocumentState.IN_REVIEW <> : DocumentEvent.SUBMIT +DocumentState.IN_REVIEW -[#1E90FF,bold]-> DocumentState.APPROVED <> : DocumentEvent.APPROVE +DocumentState.IN_REVIEW -[#1E90FF,bold]-> DocumentState.DRAFT <> : DocumentEvent.REJECT + +DocumentState.APPROVED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.scxml.xml b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.scxml.xml new file mode 100644 index 0000000..c71ad83 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/DocumentStateMachineConfiguration/DocumentStateMachineConfiguration.scxml.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.dot b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.dot new file mode 100644 index 0000000..61d8ad4 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.dot @@ -0,0 +1,12 @@ +digraph statemachine { + rankdir=LR; + node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; + edge [fontname="Arial", fontsize=10]; + + _start [shape=circle, label="", fillcolor=black, width=0.1]; + _start -> NEW; + SHIPPED [fillcolor=lightgray]; + NEW -> PENDING [label="OrderEvent.PAY / λ", style="solid", color="black"]; + PENDING -> SHIPPED [label="OrderEvent.SHIP", style="solid", color="black"]; +} + diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.json new file mode 100644 index 0000000..d911922 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.json @@ -0,0 +1,79 @@ +{ + "metadata" : { + "triggers" : [ ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/machine/{machineType}/transition/{event}", + "className" : "click.kamil.enterprise.web.StateMachineController", + "methodName" : "transition", + "sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java", + "metadata" : { + "path" : "/api/machine/{machineType}/transition/{event}", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "machineType", + "type" : "String", + "annotations" : [ "PathVariable" ] + }, { + "name" : "event", + "type" : "String", + "annotations" : [ "PathVariable" ] + }, { + "name" : "userId", + "type" : "String", + "annotations" : [ "RequestParam" ] + } ] + } ], + "callChains" : [ ], + "properties" : { + "default" : { } + } + }, + "name" : "click.kamil.enterprise.machines.order.OrderStateMachineConfiguration", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "OrderState.NEW" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderState.NEW", + "fullIdentifier" : "OrderState.NEW" + } ], + "targetStates" : [ { + "rawName" : "OrderState.PENDING", + "fullIdentifier" : "OrderState.PENDING" + } ], + "event" : { + "rawName" : "OrderEvent.PAY", + "fullIdentifier" : "OrderEvent.PAY" + }, + "guard" : null, + "actions" : [ { + "expression" : "context -> System.out.println(\"Payment processed.\")", + "isLambda" : true, + "internalLogic" : "context -> System.out.println(\"Payment processed.\")", + "lineNumber" : 28, + "className" : "click.kamil.enterprise.machines.order.OrderStateMachineConfiguration", + "sourceFile" : "src/main/java/click/kamil/enterprise/machines/order/OrderStateMachineConfiguration.java" + } ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "OrderState.PENDING", + "fullIdentifier" : "OrderState.PENDING" + } ], + "targetStates" : [ { + "rawName" : "OrderState.SHIPPED", + "fullIdentifier" : "OrderState.SHIPPED" + } ], + "event" : { + "rawName" : "OrderEvent.SHIP", + "fullIdentifier" : "OrderEvent.SHIP" + }, + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "OrderState.SHIPPED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.png b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.png new file mode 100644 index 0000000..82cd2da Binary files /dev/null and b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.png differ diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.puml new file mode 100644 index 0000000..97bbeb3 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.puml @@ -0,0 +1,32 @@ +@startuml +!pragma layout smetana +set separator none +hide empty description +hide stereotype +skinparam state { + BackgroundColor white + BorderColor #94a3b8 + BorderThickness 1 + FontName Inter + FontSize 9 + FontStyle bold + RoundCorner 20 + Padding 1 +} +skinparam shadowing false +skinparam ArrowFontName JetBrains Mono +skinparam ArrowFontSize 8 +skinparam ArrowColor #cbd5e1 +skinparam ArrowThickness 1 +skinparam dpi 110 +skinparam svgLinkTarget _self + +[*] --> OrderState.NEW + + +OrderState.NEW -[#1E90FF,bold]-> OrderState.PENDING <> : OrderEvent.PAY / λ +OrderState.PENDING -[#1E90FF,bold]-> OrderState.SHIPPED <> : OrderEvent.SHIP + +OrderState.SHIPPED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.scxml.xml b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.scxml.xml new file mode 100644 index 0000000..e05c8a3 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/OrderStateMachineConfiguration/OrderStateMachineConfiguration.scxml.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.dot b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.dot new file mode 100644 index 0000000..4f1a746 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.dot @@ -0,0 +1,12 @@ +digraph statemachine { + rankdir=LR; + node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"]; + edge [fontname="Arial", fontsize=10]; + + _start [shape=circle, label="", fillcolor=black, width=0.1]; + _start -> GUEST; + VERIFIED [fillcolor=lightgray]; + GUEST -> REGISTERED [label="UserEvent.REGISTER", style="solid", color="black"]; + REGISTERED -> VERIFIED [label="UserEvent.VERIFY", style="solid", color="black"]; +} + diff --git a/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.json new file mode 100644 index 0000000..6f04727 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.json @@ -0,0 +1,72 @@ +{ + "metadata" : { + "triggers" : [ ], + "entryPoints" : [ { + "type" : "REST", + "name" : "POST /api/machine/{machineType}/transition/{event}", + "className" : "click.kamil.enterprise.web.StateMachineController", + "methodName" : "transition", + "sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java", + "metadata" : { + "path" : "/api/machine/{machineType}/transition/{event}", + "verb" : "POST" + }, + "parameters" : [ { + "name" : "machineType", + "type" : "String", + "annotations" : [ "PathVariable" ] + }, { + "name" : "event", + "type" : "String", + "annotations" : [ "PathVariable" ] + }, { + "name" : "userId", + "type" : "String", + "annotations" : [ "RequestParam" ] + } ] + } ], + "callChains" : [ ], + "properties" : { + "default" : { } + } + }, + "name" : "click.kamil.enterprise.machines.user.UserStateMachineConfiguration", + "renderChoicesAsDiamonds" : true, + "startStates" : [ "UserState.GUEST" ], + "transitions" : [ { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "UserState.GUEST", + "fullIdentifier" : "UserState.GUEST" + } ], + "targetStates" : [ { + "rawName" : "UserState.REGISTERED", + "fullIdentifier" : "UserState.REGISTERED" + } ], + "event" : { + "rawName" : "UserEvent.REGISTER", + "fullIdentifier" : "UserEvent.REGISTER" + }, + "guard" : null, + "actions" : [ ], + "order" : null + }, { + "type" : "EXTERNAL", + "sourceStates" : [ { + "rawName" : "UserState.REGISTERED", + "fullIdentifier" : "UserState.REGISTERED" + } ], + "targetStates" : [ { + "rawName" : "UserState.VERIFIED", + "fullIdentifier" : "UserState.VERIFIED" + } ], + "event" : { + "rawName" : "UserEvent.VERIFY", + "fullIdentifier" : "UserEvent.VERIFY" + }, + "guard" : null, + "actions" : [ ], + "order" : null + } ], + "endStates" : [ "UserState.VERIFIED" ] +} diff --git a/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.png b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.png new file mode 100644 index 0000000..2231de8 Binary files /dev/null and b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.png differ diff --git a/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.puml b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.puml new file mode 100644 index 0000000..366ce20 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.puml @@ -0,0 +1,32 @@ +@startuml +!pragma layout smetana +set separator none +hide empty description +hide stereotype +skinparam state { + BackgroundColor white + BorderColor #94a3b8 + BorderThickness 1 + FontName Inter + FontSize 9 + FontStyle bold + RoundCorner 20 + Padding 1 +} +skinparam shadowing false +skinparam ArrowFontName JetBrains Mono +skinparam ArrowFontSize 8 +skinparam ArrowColor #cbd5e1 +skinparam ArrowThickness 1 +skinparam dpi 110 +skinparam svgLinkTarget _self + +[*] --> UserState.GUEST + + +UserState.GUEST -[#1E90FF,bold]-> UserState.REGISTERED <> : UserEvent.REGISTER +UserState.REGISTERED -[#1E90FF,bold]-> UserState.VERIFIED <> : UserEvent.VERIFY + +UserState.VERIFIED --> [*] +@enduml + diff --git a/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.scxml.xml b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.scxml.xml new file mode 100644 index 0000000..b70b364 --- /dev/null +++ b/state_machine_exporter/src/test/resources/golden/UserStateMachineConfiguration/UserStateMachineConfiguration.scxml.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/state_machines/state_machine_enterprise/build.gradle b/state_machines/state_machine_enterprise/build.gradle new file mode 100644 index 0000000..9772ac9 --- /dev/null +++ b/state_machines/state_machine_enterprise/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'click.kamil' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' +} + +bootJar { enabled = false } +jar { enabled = true } diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/EnterpriseStateMachineApplication.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/EnterpriseStateMachineApplication.java new file mode 100644 index 0000000..7dec8e6 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/EnterpriseStateMachineApplication.java @@ -0,0 +1,11 @@ +package click.kamil.enterprise; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EnterpriseStateMachineApplication { + public static void main(String[] args) { + SpringApplication.run(EnterpriseStateMachineApplication.class, args); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/AbstractStateMachine.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/AbstractStateMachine.java new file mode 100644 index 0000000..0668791 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/AbstractStateMachine.java @@ -0,0 +1,50 @@ +package click.kamil.enterprise.core; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class AbstractStateMachine & EnterpriseState, E extends Enum & EnterpriseEvent> { + + private S currentState; + private final Map transitions = new ConcurrentHashMap<>(); + + protected AbstractStateMachine(S initialState) { + this.currentState = initialState; + configureTransitions(); + } + + public abstract String getMachineType(); + + protected abstract void configureTransitions(); + + protected void addTransition(E event, S targetState) { + transitions.put(event, targetState); + } + + public synchronized void trigger(E event, TransitionContext context) { + S target = transitions.get(event); + if (target == null) { + throw new IllegalStateException("Invalid event " + event + " for state " + currentState); + } + + executePreChecks(currentState, target, context); + S oldState = currentState; + currentState = target; + onTransition(oldState, currentState, event, context); + } + + protected void executePreChecks(S from, S to, TransitionContext context) { + // Parent checking logic + System.out.println("[Abstract] Executing pre-checks from " + from.name() + " to " + to.name() + " by user " + context.getUserId()); + } + + protected abstract void onTransition(S from, S to, E event, TransitionContext context); + + public S getCurrentState() { + return currentState; + } + + public Class getEventType() { + return (Class) transitions.keySet().iterator().next().getClass(); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/AbstractStateMachineConfiguration.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/AbstractStateMachineConfiguration.java new file mode 100644 index 0000000..af44378 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/AbstractStateMachineConfiguration.java @@ -0,0 +1,25 @@ +package click.kamil.enterprise.core; + +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +public abstract class AbstractStateMachineConfiguration & EnterpriseState, E extends Enum & EnterpriseEvent> extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + configureStates(states); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + configureTransitions(transitions); + } + + protected abstract void configureStates(StateMachineStateConfigurer states) throws Exception; + protected abstract void configureTransitions(StateMachineTransitionConfigurer transitions) throws Exception; + + protected void commonProtectedMethod() { + System.out.println("Common protected method called in AbstractStateMachineConfiguration."); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/EnterpriseEvent.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/EnterpriseEvent.java new file mode 100644 index 0000000..9e9a9f3 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/EnterpriseEvent.java @@ -0,0 +1,5 @@ +package click.kamil.enterprise.core; + +public interface EnterpriseEvent { + String name(); +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/EnterpriseState.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/EnterpriseState.java new file mode 100644 index 0000000..8d88dc7 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/EnterpriseState.java @@ -0,0 +1,5 @@ +package click.kamil.enterprise.core; + +public interface EnterpriseState { + String name(); +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/TransitionContext.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/TransitionContext.java new file mode 100644 index 0000000..9dbb78e --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/core/TransitionContext.java @@ -0,0 +1,17 @@ +package click.kamil.enterprise.core; + +import lombok.Data; +import java.time.Instant; + +@Data +public class TransitionContext { + private final String userId; + private final Instant timestamp; + private final T payload; + + public TransitionContext(String userId, T payload) { + this.userId = userId; + this.timestamp = Instant.now(); + this.payload = payload; + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentEvent.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentEvent.java new file mode 100644 index 0000000..8bdeb08 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentEvent.java @@ -0,0 +1,7 @@ +package click.kamil.enterprise.machines.document; + +import click.kamil.enterprise.core.EnterpriseEvent; + +public enum DocumentEvent implements EnterpriseEvent { + SUBMIT, APPROVE, REJECT; +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentState.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentState.java new file mode 100644 index 0000000..6f5865f --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentState.java @@ -0,0 +1,7 @@ +package click.kamil.enterprise.machines.document; + +import click.kamil.enterprise.core.EnterpriseState; + +public enum DocumentState implements EnterpriseState { + DRAFT, IN_REVIEW, APPROVED; +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentStateMachine.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentStateMachine.java new file mode 100644 index 0000000..3af338b --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentStateMachine.java @@ -0,0 +1,30 @@ +package click.kamil.enterprise.machines.document; + +import click.kamil.enterprise.core.AbstractStateMachine; +import click.kamil.enterprise.core.TransitionContext; +import org.springframework.stereotype.Component; + +@Component +public class DocumentStateMachine extends AbstractStateMachine { + + public DocumentStateMachine() { + super(DocumentState.DRAFT); + } + + @Override + public String getMachineType() { + return "DOCUMENT"; + } + + @Override + protected void configureTransitions() { + addTransition(DocumentEvent.SUBMIT, DocumentState.IN_REVIEW); + addTransition(DocumentEvent.APPROVE, DocumentState.APPROVED); + addTransition(DocumentEvent.REJECT, DocumentState.DRAFT); + } + + @Override + protected void onTransition(DocumentState from, DocumentState to, DocumentEvent event, TransitionContext context) { + System.out.println("[Document] Document modified by " + context.getUserId() + ". Now " + to); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentStateMachineConfiguration.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentStateMachineConfiguration.java new file mode 100644 index 0000000..686f780 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/document/DocumentStateMachineConfiguration.java @@ -0,0 +1,35 @@ +package click.kamil.enterprise.machines.document; + +import click.kamil.enterprise.core.AbstractStateMachineConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachineFactory; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +@Configuration +@EnableStateMachineFactory(name = "documentStateMachineFactory") +public class DocumentStateMachineConfiguration extends AbstractStateMachineConfiguration { + + @Override + protected void configureStates(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(DocumentState.DRAFT) + .state(DocumentState.IN_REVIEW) + .end(DocumentState.APPROVED); + } + + @Override + protected void configureTransitions(StateMachineTransitionConfigurer transitions) throws Exception { + commonProtectedMethod(); + transitions + .withExternal() + .source(DocumentState.DRAFT).target(DocumentState.IN_REVIEW).event(DocumentEvent.SUBMIT) + .and() + .withExternal() + .source(DocumentState.IN_REVIEW).target(DocumentState.APPROVED).event(DocumentEvent.APPROVE) + .and() + .withExternal() + .source(DocumentState.IN_REVIEW).target(DocumentState.DRAFT).event(DocumentEvent.REJECT); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderEvent.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderEvent.java new file mode 100644 index 0000000..21fdec6 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderEvent.java @@ -0,0 +1,7 @@ +package click.kamil.enterprise.machines.order; + +import click.kamil.enterprise.core.EnterpriseEvent; + +public enum OrderEvent implements EnterpriseEvent { + PAY, SHIP; +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderState.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderState.java new file mode 100644 index 0000000..b46448a --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderState.java @@ -0,0 +1,7 @@ +package click.kamil.enterprise.machines.order; + +import click.kamil.enterprise.core.EnterpriseState; + +public enum OrderState implements EnterpriseState { + NEW, PENDING, SHIPPED; +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderStateMachine.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderStateMachine.java new file mode 100644 index 0000000..1b25fac --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderStateMachine.java @@ -0,0 +1,38 @@ +package click.kamil.enterprise.machines.order; + +import click.kamil.enterprise.core.AbstractStateMachine; +import click.kamil.enterprise.core.TransitionContext; +import org.springframework.stereotype.Component; + +@Component +public class OrderStateMachine extends AbstractStateMachine { + + public OrderStateMachine() { + super(OrderState.NEW); + } + + @Override + public String getMachineType() { + return "ORDER"; + } + + @Override + protected void configureTransitions() { + addTransition(OrderEvent.PAY, OrderState.PENDING); + addTransition(OrderEvent.SHIP, OrderState.SHIPPED); + } + + @Override + protected void executePreChecks(OrderState from, OrderState to, TransitionContext context) { + super.executePreChecks(from, to, context); + if (to == OrderState.SHIPPED && from != OrderState.PENDING) { + throw new IllegalStateException("Must be PENDING to ship!"); + } + System.out.println("[Order] Specific pre-checks passed for order."); + } + + @Override + protected void onTransition(OrderState from, OrderState to, OrderEvent event, TransitionContext context) { + System.out.println("[Order] Transitioned from " + from + " to " + to + " via " + event); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderStateMachineConfiguration.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderStateMachineConfiguration.java new file mode 100644 index 0000000..54781c8 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/order/OrderStateMachineConfiguration.java @@ -0,0 +1,33 @@ +package click.kamil.enterprise.machines.order; + +import click.kamil.enterprise.core.AbstractStateMachineConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachineFactory; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +@Configuration +@EnableStateMachineFactory(name = "orderStateMachineFactory") +public class OrderStateMachineConfiguration extends AbstractStateMachineConfiguration { + + @Override + protected void configureStates(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(OrderState.NEW) + .state(OrderState.PENDING) + .end(OrderState.SHIPPED); + } + + @Override + protected void configureTransitions(StateMachineTransitionConfigurer transitions) throws Exception { + commonProtectedMethod(); + transitions + .withExternal() + .source(OrderState.NEW).target(OrderState.PENDING).event(OrderEvent.PAY) + .action(context -> System.out.println("Payment processed.")) + .and() + .withExternal() + .source(OrderState.PENDING).target(OrderState.SHIPPED).event(OrderEvent.SHIP); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserEvent.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserEvent.java new file mode 100644 index 0000000..72849ec --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserEvent.java @@ -0,0 +1,7 @@ +package click.kamil.enterprise.machines.user; + +import click.kamil.enterprise.core.EnterpriseEvent; + +public enum UserEvent implements EnterpriseEvent { + REGISTER, VERIFY; +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserState.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserState.java new file mode 100644 index 0000000..dd601f7 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserState.java @@ -0,0 +1,7 @@ +package click.kamil.enterprise.machines.user; + +import click.kamil.enterprise.core.EnterpriseState; + +public enum UserState implements EnterpriseState { + GUEST, REGISTERED, VERIFIED; +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserStateMachine.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserStateMachine.java new file mode 100644 index 0000000..9719088 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserStateMachine.java @@ -0,0 +1,29 @@ +package click.kamil.enterprise.machines.user; + +import click.kamil.enterprise.core.AbstractStateMachine; +import click.kamil.enterprise.core.TransitionContext; +import org.springframework.stereotype.Component; + +@Component +public class UserStateMachine extends AbstractStateMachine { + + public UserStateMachine() { + super(UserState.GUEST); + } + + @Override + public String getMachineType() { + return "USER"; + } + + @Override + protected void configureTransitions() { + addTransition(UserEvent.REGISTER, UserState.REGISTERED); + addTransition(UserEvent.VERIFY, UserState.VERIFIED); + } + + @Override + protected void onTransition(UserState from, UserState to, UserEvent event, TransitionContext context) { + System.out.println("[User] " + context.getUserId() + " is now " + to); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserStateMachineConfiguration.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserStateMachineConfiguration.java new file mode 100644 index 0000000..b0747d9 --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/machines/user/UserStateMachineConfiguration.java @@ -0,0 +1,32 @@ +package click.kamil.enterprise.machines.user; + +import click.kamil.enterprise.core.AbstractStateMachineConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachineFactory; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +@Configuration +@EnableStateMachineFactory(name = "userStateMachineFactory") +public class UserStateMachineConfiguration extends AbstractStateMachineConfiguration { + + @Override + protected void configureStates(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(UserState.GUEST) + .state(UserState.REGISTERED) + .end(UserState.VERIFIED); + } + + @Override + protected void configureTransitions(StateMachineTransitionConfigurer transitions) throws Exception { + commonProtectedMethod(); + transitions + .withExternal() + .source(UserState.GUEST).target(UserState.REGISTERED).event(UserEvent.REGISTER) + .and() + .withExternal() + .source(UserState.REGISTERED).target(UserState.VERIFIED).event(UserEvent.VERIFY); + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/web/StateMachineController.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/web/StateMachineController.java new file mode 100644 index 0000000..4b8e5bb --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/web/StateMachineController.java @@ -0,0 +1,31 @@ +package click.kamil.enterprise.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/machine") +public class StateMachineController { + + private final StateMachineDispatcher dispatcher; + + @Autowired + public StateMachineController(StateMachineDispatcher dispatcher) { + this.dispatcher = dispatcher; + } + + @PostMapping("/{machineType}/transition/{event}") + public ResponseEntity transition( + @PathVariable String machineType, + @PathVariable String event, + @RequestParam(defaultValue = "system-user") String userId) { + + try { + String result = dispatcher.dispatch(machineType, event, userId, null); + return ResponseEntity.ok(result); + } catch (Exception e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } +} diff --git a/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java new file mode 100644 index 0000000..992f46b --- /dev/null +++ b/state_machines/state_machine_enterprise/src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java @@ -0,0 +1,45 @@ +package click.kamil.enterprise.web; + +import click.kamil.enterprise.core.AbstractStateMachine; +import click.kamil.enterprise.core.EnterpriseEvent; +import click.kamil.enterprise.core.TransitionContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class StateMachineDispatcher { + + private final Map> registry = new HashMap<>(); + + @Autowired + public StateMachineDispatcher(List> machines) { + for (AbstractStateMachine machine : machines) { + registry.put(machine.getMachineType().toUpperCase(), machine); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public String dispatch(String machineType, String eventString, String userId, Object payload) { + AbstractStateMachine machine = registry.get(machineType.toUpperCase()); + if (machine == null) { + throw new IllegalArgumentException("Unknown machine type: " + machineType); + } + + Class eventType = (Class) machine.getEventType(); + Enum eventEnum; + try { + eventEnum = Enum.valueOf(eventType, eventString.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid event " + eventString + " for machine " + machineType); + } + + TransitionContext context = new TransitionContext<>(userId, payload); + machine.trigger((EnterpriseEvent) eventEnum, context); + + return "Triggered " + eventEnum.name() + " on " + machineType + ". New State: " + machine.getCurrentState().name(); + } +}