another test state machine
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package click.kamil.enterprise.core;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public abstract class AbstractStateMachine<S extends Enum<S> & EnterpriseState, E extends Enum<E> & EnterpriseEvent> {
|
||||
|
||||
private S currentState;
|
||||
private final Map<E, S> 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<E> getEventType() {
|
||||
return (Class<E>) transitions.keySet().iterator().next().getClass();
|
||||
}
|
||||
}
|
||||
@@ -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<S extends Enum<S> & EnterpriseState, E extends Enum<E> & EnterpriseEvent> extends EnumStateMachineConfigurerAdapter<S, E> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<S, E> states) throws Exception {
|
||||
configureStates(states);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<S, E> transitions) throws Exception {
|
||||
configureTransitions(transitions);
|
||||
}
|
||||
|
||||
protected abstract void configureStates(StateMachineStateConfigurer<S, E> states) throws Exception;
|
||||
protected abstract void configureTransitions(StateMachineTransitionConfigurer<S, E> transitions) throws Exception;
|
||||
|
||||
protected void commonProtectedMethod() {
|
||||
System.out.println("Common protected method called in AbstractStateMachineConfiguration.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package click.kamil.enterprise.core;
|
||||
|
||||
public interface EnterpriseEvent {
|
||||
String name();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package click.kamil.enterprise.core;
|
||||
|
||||
public interface EnterpriseState {
|
||||
String name();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package click.kamil.enterprise.core;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
public class TransitionContext<T> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.enterprise.machines.document;
|
||||
|
||||
import click.kamil.enterprise.core.EnterpriseEvent;
|
||||
|
||||
public enum DocumentEvent implements EnterpriseEvent {
|
||||
SUBMIT, APPROVE, REJECT;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<DocumentState, DocumentEvent> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<DocumentState, DocumentEvent> {
|
||||
|
||||
@Override
|
||||
protected void configureStates(StateMachineStateConfigurer<DocumentState, DocumentEvent> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(DocumentState.DRAFT)
|
||||
.state(DocumentState.IN_REVIEW)
|
||||
.end(DocumentState.APPROVED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureTransitions(StateMachineTransitionConfigurer<DocumentState, DocumentEvent> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.enterprise.machines.order;
|
||||
|
||||
import click.kamil.enterprise.core.EnterpriseEvent;
|
||||
|
||||
public enum OrderEvent implements EnterpriseEvent {
|
||||
PAY, SHIP;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.enterprise.machines.order;
|
||||
|
||||
import click.kamil.enterprise.core.EnterpriseState;
|
||||
|
||||
public enum OrderState implements EnterpriseState {
|
||||
NEW, PENDING, SHIPPED;
|
||||
}
|
||||
@@ -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<OrderState, OrderEvent> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<OrderState, OrderEvent> {
|
||||
|
||||
@Override
|
||||
protected void configureStates(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(OrderState.NEW)
|
||||
.state(OrderState.PENDING)
|
||||
.end(OrderState.SHIPPED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureTransitions(StateMachineTransitionConfigurer<OrderState, OrderEvent> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.enterprise.machines.user;
|
||||
|
||||
import click.kamil.enterprise.core.EnterpriseEvent;
|
||||
|
||||
public enum UserEvent implements EnterpriseEvent {
|
||||
REGISTER, VERIFY;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package click.kamil.enterprise.machines.user;
|
||||
|
||||
import click.kamil.enterprise.core.EnterpriseState;
|
||||
|
||||
public enum UserState implements EnterpriseState {
|
||||
GUEST, REGISTERED, VERIFIED;
|
||||
}
|
||||
@@ -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<UserState, UserEvent> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<UserState, UserEvent> {
|
||||
|
||||
@Override
|
||||
protected void configureStates(StateMachineStateConfigurer<UserState, UserEvent> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(UserState.GUEST)
|
||||
.state(UserState.REGISTERED)
|
||||
.end(UserState.VERIFIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureTransitions(StateMachineTransitionConfigurer<UserState, UserEvent> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, AbstractStateMachine<?, ?>> registry = new HashMap<>();
|
||||
|
||||
@Autowired
|
||||
public StateMachineDispatcher(List<AbstractStateMachine<?, ?>> 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<? extends Enum> eventType = (Class<? extends Enum>) machine.getEventType();
|
||||
Enum eventEnum;
|
||||
try {
|
||||
eventEnum = Enum.valueOf(eventType, eventString.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid event " + eventString + " for machine " + machineType);
|
||||
}
|
||||
|
||||
TransitionContext<Object> context = new TransitionContext<>(userId, payload);
|
||||
machine.trigger((EnterpriseEvent) eventEnum, context);
|
||||
|
||||
return "Triggered " + eventEnum.name() + " on " + machineType + ". New State: " + machine.getCurrentState().name();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user