├── .gitignore ├── README.md ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── antkorwin │ │ │ └── statemachine │ │ │ ├── StatemachineApplication.java │ │ │ ├── config │ │ │ ├── PersistConfig.java │ │ │ ├── SecondStateMachineConfig.java │ │ │ └── StateMachineConfig.java │ │ │ ├── persist │ │ │ ├── CustomStateMachinePersist.java │ │ │ └── InMemoryStateMachinePersist.java │ │ │ └── statemachine │ │ │ ├── Events.java │ │ │ ├── States.java │ │ │ └── resolver │ │ │ ├── StateMachineResolver.java │ │ │ └── StateMachineResolverImpl.java │ └── resources │ │ └── application.properties └── test │ ├── java │ └── com │ │ └── antkorwin │ │ └── statemachine │ │ ├── BaseMongoIT.java │ │ ├── InMemoryPersistTest.java │ │ ├── MongoPersistTest.java │ │ ├── MultipleFactoriesTest.java │ │ ├── StatemachineApplicationTests.java │ │ └── statemachine │ │ └── resolver │ │ └── StateMachineResolverIT.java │ └── resources │ ├── application.properties │ └── logback-test.xml └── uml └── state_graph.puml /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring state machine 2 | Spring State Machine Workshop 3 | 4 | * consider a simple linear flow for Kanban board 5 | 6 | * add some actions on the states. 7 | 8 | * add guard for "testing" state. 9 | 10 | * example of communication based on the variables of state 11 | 12 | * simple persist (in-memory) 13 | 14 | * persist state machine in mongodb 15 | 16 | * multiple state machine in one project 17 | 18 | * evaluate available events from current state 19 | 20 | Read full article in my blog : http://antkorwin.com/statemachine/statemachine.html -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.antkorwin 7 | statemachine 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | statemachine 12 | Spring State Machine Workshop 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.1.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 2.0.1.RELEASE 26 | 27 | 28 | 29 | 30 | org.springframework.statemachine 31 | spring-statemachine-starter 32 | 33 | 34 | org.projectlombok 35 | lombok 36 | true 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-test 41 | test 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-data-mongodb 48 | 49 | 50 | org.springframework.statemachine 51 | spring-statemachine-data-mongodb 52 | 2.0.0.RELEASE 53 | 54 | 55 | 56 | 57 | 58 | org.testcontainers 59 | testcontainers 60 | 1.4.3 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.springframework.statemachine 71 | spring-statemachine-bom 72 | ${spring-statemachine.version} 73 | pom 74 | import 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-maven-plugin 84 | 85 | 86 | 87 | 88 | ${project.basedir}/src/main/resources 89 | true 90 | 91 | 92 | 93 | 94 | ${project.basedir}/src/test/resources 95 | true 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/StatemachineApplication.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class StatemachineApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(StatemachineApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/config/PersistConfig.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.config; 2 | 3 | import com.antkorwin.statemachine.persist.CustomStateMachinePersist; 4 | import com.antkorwin.statemachine.persist.InMemoryStateMachinePersist; 5 | import com.antkorwin.statemachine.statemachine.Events; 6 | import com.antkorwin.statemachine.statemachine.States; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.statemachine.StateMachinePersist; 11 | import org.springframework.statemachine.data.mongodb.MongoDbPersistingStateMachineInterceptor; 12 | import org.springframework.statemachine.data.mongodb.MongoDbStateMachineRepository; 13 | import org.springframework.statemachine.persist.DefaultStateMachinePersister; 14 | import org.springframework.statemachine.persist.StateMachinePersister; 15 | import org.springframework.statemachine.persist.StateMachineRuntimePersister; 16 | 17 | import java.util.UUID; 18 | 19 | /** 20 | * Created by Korovin Anatolii on 09.05.2018. 21 | * 22 | * @author Korovin Anatolii 23 | * @version 1.0 24 | */ 25 | @Configuration 26 | public class PersistConfig { 27 | 28 | @Bean 29 | @Profile({"in-memory", "default"}) 30 | public StateMachinePersist inMemoryPersist() { 31 | return new InMemoryStateMachinePersist(); 32 | } 33 | 34 | @Bean 35 | @Profile("custom") 36 | public StateMachinePersist customPersist() { 37 | return new CustomStateMachinePersist(); 38 | } 39 | 40 | @Bean 41 | @Profile("mongo") 42 | public StateMachineRuntimePersister mongoRuntimePersist( 43 | MongoDbStateMachineRepository repository) { 44 | return new MongoDbPersistingStateMachineInterceptor<>(repository); 45 | } 46 | 47 | @Bean 48 | public StateMachinePersister persister( 49 | StateMachinePersist defaultPersist) { 50 | 51 | return new DefaultStateMachinePersister<>(defaultPersist); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/config/SecondStateMachineConfig.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.config; 2 | 3 | import com.antkorwin.statemachine.statemachine.Events; 4 | import com.antkorwin.statemachine.statemachine.States; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.messaging.Message; 8 | import org.springframework.statemachine.action.Action; 9 | import org.springframework.statemachine.config.EnableStateMachineFactory; 10 | import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; 11 | import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; 12 | import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; 13 | import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; 14 | import org.springframework.statemachine.guard.Guard; 15 | import org.springframework.statemachine.listener.StateMachineListener; 16 | import org.springframework.statemachine.listener.StateMachineListenerAdapter; 17 | import org.springframework.statemachine.state.State; 18 | import org.springframework.statemachine.transition.Transition; 19 | 20 | import java.util.Optional; 21 | 22 | /** 23 | * Created by Korovin Anatolii on 05.05.2018. 24 | *

25 | * Second state machine, you can: 26 | * - use this other states and events 27 | * - make other business logic configuration 28 | * 29 | * @author Korovin Anatolii 30 | * @version 1.0 31 | */ 32 | @Slf4j 33 | @Configuration 34 | @EnableStateMachineFactory(name = "secondStateMachineFactory") 35 | public class SecondStateMachineConfig extends EnumStateMachineConfigurerAdapter { 36 | 37 | @Override 38 | public void configure(StateMachineConfigurationConfigurer config) throws Exception { 39 | config.withConfiguration() 40 | .listener(listener()) 41 | .autoStartup(true); 42 | } 43 | 44 | private StateMachineListener listener() { 45 | 46 | return new StateMachineListenerAdapter() { 47 | @Override 48 | public void eventNotAccepted(Message event) { 49 | log.error("SECOND: Not accepted event: {}", event); 50 | } 51 | 52 | @Override 53 | public void transition(Transition transition) { 54 | log.info("TRANS from: {} to: {}", 55 | ofNullableState(transition.getSource()), 56 | ofNullableState(transition.getTarget())); 57 | } 58 | 59 | private Object ofNullableState(State s) { 60 | return Optional.ofNullable(s) 61 | .map(State::getId) 62 | .orElse(null); 63 | } 64 | }; 65 | } 66 | 67 | @Override 68 | public void configure(StateMachineStateConfigurer states) throws Exception { 69 | states.withStates() 70 | .initial(States.BACKLOG) 71 | .end(States.DONE); 72 | } 73 | 74 | @Override 75 | public void configure(StateMachineTransitionConfigurer transitions) throws Exception { 76 | transitions.withExternal() 77 | // BACKLOG --> DONE : START_FEATURE 78 | .source(States.BACKLOG) 79 | .target(States.DONE) 80 | .event(Events.START_FEATURE) 81 | .guard(failGuard()) 82 | .and() 83 | // BACKLOG --> DONE : ROCK_STAR_DOUBLE_TASK 84 | .withExternal() 85 | .source(States.BACKLOG) 86 | .target(States.DONE) 87 | .action(failAction()) 88 | .event(Events.ROCK_STAR_DOUBLE_TASK) 89 | .and() 90 | // BACKLOG --> TESTING : FINISH_FEATURE 91 | .withExternal() 92 | .source(States.BACKLOG) 93 | .target(States.DONE) 94 | .action(actionWithInternalError()) 95 | .event(Events.FINISH_FEATURE); 96 | } 97 | 98 | private Action failAction() { 99 | return context -> { 100 | throw new RuntimeException("fail in action"); 101 | }; 102 | } 103 | 104 | private Guard failGuard() { 105 | return context -> { 106 | throw new RuntimeException("fail in Guard"); 107 | }; 108 | } 109 | 110 | private Action actionWithInternalError() { 111 | return context -> { 112 | context.getStateMachine() 113 | .setStateMachineError(new RuntimeException("fail in Action")); 114 | }; 115 | } 116 | 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/config/StateMachineConfig.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.config; 2 | 3 | import com.antkorwin.statemachine.statemachine.Events; 4 | import com.antkorwin.statemachine.statemachine.States; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.messaging.Message; 8 | import org.springframework.statemachine.action.Action; 9 | import org.springframework.statemachine.config.EnableStateMachineFactory; 10 | import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; 11 | import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; 12 | import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; 13 | import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; 14 | import org.springframework.statemachine.guard.Guard; 15 | import org.springframework.statemachine.listener.StateMachineListener; 16 | import org.springframework.statemachine.listener.StateMachineListenerAdapter; 17 | import org.springframework.statemachine.state.State; 18 | import org.springframework.statemachine.transition.Transition; 19 | 20 | import java.util.Optional; 21 | 22 | /** 23 | * Created by Korovin Anatolii on 05.05.2018. 24 | * 25 | * @author Korovin Anatolii 26 | * @version 1.0 27 | */ 28 | @Slf4j 29 | @Configuration 30 | @EnableStateMachineFactory 31 | public class StateMachineConfig extends EnumStateMachineConfigurerAdapter { 32 | 33 | @Override 34 | public void configure(StateMachineConfigurationConfigurer config) throws Exception { 35 | config.withConfiguration() 36 | .listener(listener()) 37 | .autoStartup(true); 38 | } 39 | 40 | private StateMachineListener listener() { 41 | 42 | return new StateMachineListenerAdapter() { 43 | @Override 44 | public void eventNotAccepted(Message event) { 45 | log.error("Not accepted event: {}", event); 46 | } 47 | 48 | @Override 49 | public void transition(Transition transition) { 50 | log.warn("MOVE from: {}, to: {}", 51 | ofNullableState(transition.getSource()), 52 | ofNullableState(transition.getTarget())); 53 | } 54 | 55 | private Object ofNullableState(State s) { 56 | return Optional.ofNullable(s) 57 | .map(State::getId) 58 | .orElse(null); 59 | } 60 | }; 61 | } 62 | 63 | 64 | @Override 65 | public void configure(StateMachineStateConfigurer states) throws Exception { 66 | states.withStates() 67 | .initial(States.BACKLOG, developersWakeUpAction()) 68 | .state(States.IN_PROGRESS, weNeedCoffeeAction()) 69 | .state(States.TESTING, qaWakeUpAction()) 70 | .state(States.DONE, goToSleepAction()) 71 | .end(States.DONE); 72 | } 73 | 74 | private Action developersWakeUpAction() { 75 | return stateContext -> log.warn("Просыпайтесь лентяи!"); 76 | } 77 | 78 | private Action weNeedCoffeeAction() { 79 | return stateContext -> log.warn("Без кофе никак!"); 80 | } 81 | 82 | private Action qaWakeUpAction() { 83 | return stateContext -> log.warn("Будим команду тестирования, солнце высоко!"); 84 | } 85 | 86 | private Action goToSleepAction() { 87 | return stateContext -> log.warn("Всем спать! клиент доволен."); 88 | } 89 | 90 | @Override 91 | public void configure(StateMachineTransitionConfigurer transitions) throws Exception { 92 | transitions.withExternal() 93 | .source(States.BACKLOG) 94 | .target(States.IN_PROGRESS) 95 | .event(Events.START_FEATURE) 96 | .and() 97 | // DEVELOPERS: 98 | .withExternal() 99 | .source(States.IN_PROGRESS) 100 | .target(States.TESTING) 101 | .event(Events.FINISH_FEATURE) 102 | .guard(alreadyDeployedGuard()) 103 | .and() 104 | // QA-TEAM: 105 | .withExternal() 106 | .source(States.TESTING) 107 | .target(States.DONE) 108 | .event(Events.QA_CHECKED_UC) 109 | .and() 110 | .withExternal() 111 | .source(States.TESTING) 112 | .target(States.IN_PROGRESS) 113 | .event(Events.QA_REJECTED_UC) 114 | .and() 115 | // ROCK-STAR: 116 | .withExternal() 117 | .source(States.BACKLOG) 118 | .target(States.TESTING) 119 | .event(Events.ROCK_STAR_DOUBLE_TASK) 120 | .and() 121 | // DEVOPS: 122 | .withInternal() 123 | .source(States.IN_PROGRESS) 124 | .event(Events.DEPLOY) 125 | .action(deployPreProd()) 126 | .and() 127 | .withInternal() 128 | .source(States.BACKLOG) 129 | .event(Events.DEPLOY) 130 | .action(deployPreProd()); 131 | } 132 | 133 | private Guard alreadyDeployedGuard() { 134 | return context -> Optional.ofNullable(context.getExtendedState().getVariables().get("deployed")) 135 | .map(v -> (boolean) v) 136 | .orElse(false); 137 | } 138 | 139 | private Action deployPreProd() { 140 | return stateContext -> { 141 | log.warn("DEPLOY: Выкатываемся на препродакшен."); 142 | stateContext.getExtendedState().getVariables().put("deployed", true); 143 | }; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/persist/CustomStateMachinePersist.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.persist; 2 | 3 | import com.antkorwin.statemachine.statemachine.Events; 4 | import com.antkorwin.statemachine.statemachine.States; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.statemachine.StateMachineContext; 7 | import org.springframework.statemachine.StateMachinePersist; 8 | 9 | import java.util.HashMap; 10 | import java.util.UUID; 11 | 12 | /** 13 | * Created by Korovin Anatolii on 06.05.2018. 14 | * 15 | * @author Korovin Anatolii 16 | * @version 1.0 17 | */ 18 | @Slf4j 19 | public class CustomStateMachinePersist implements StateMachinePersist { 20 | 21 | private HashMap> storage = new HashMap<>(); 22 | 23 | @Override 24 | public void write(StateMachineContext context, UUID contextObj) throws Exception { 25 | log.warn("!!!push: "+contextObj); 26 | storage.put(contextObj, context); 27 | } 28 | 29 | @Override 30 | public StateMachineContext read(UUID contextObj) throws Exception { 31 | log.warn("!!!pop: "+contextObj); 32 | return storage.get(contextObj); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/persist/InMemoryStateMachinePersist.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.persist; 2 | 3 | 4 | import com.antkorwin.statemachine.statemachine.Events; 5 | import com.antkorwin.statemachine.statemachine.States; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.statemachine.StateMachineContext; 8 | import org.springframework.statemachine.StateMachinePersist; 9 | 10 | import java.util.HashMap; 11 | import java.util.UUID; 12 | 13 | /** 14 | * Created by Korovin Anatolii on 06.05.2018. 15 | * 16 | * @author Korovin Anatolii 17 | * @version 1.0 18 | */ 19 | @Slf4j 20 | public class InMemoryStateMachinePersist implements StateMachinePersist { 21 | 22 | private HashMap> storage = new HashMap<>(); 23 | 24 | @Override 25 | public void write(StateMachineContext context, UUID contextObj) throws Exception { 26 | storage.put(contextObj, context); 27 | } 28 | 29 | @Override 30 | public StateMachineContext read(UUID contextObj) throws Exception { 31 | return storage.get(contextObj); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/statemachine/Events.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.statemachine; 2 | 3 | /** 4 | * Created by Korovin Anatolii on 05.05.2018. 5 | * 6 | * @author Korovin Anatolii 7 | * @version 1.0 8 | */ 9 | public enum Events { 10 | START_FEATURE, 11 | FINISH_FEATURE, 12 | QA_REJECTED_UC, 13 | ROCK_STAR_DOUBLE_TASK, 14 | DEPLOY, 15 | QA_CHECKED_UC 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/statemachine/States.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.statemachine; 2 | 3 | /** 4 | * Created by Korovin Anatolii on 05.05.2018. 5 | * 6 | * @author Korovin Anatolii 7 | * @version 1.0 8 | */ 9 | public enum States { 10 | BACKLOG, 11 | IN_PROGRESS, 12 | TESTING, 13 | DONE 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/statemachine/resolver/StateMachineResolver.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.statemachine.resolver; 2 | 3 | import org.springframework.statemachine.StateMachine; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Created by Korovin Anatolii on 02.06.2018. 9 | * 10 | * @author Korovin Anatolii 11 | * @version 1.0 12 | */ 13 | public interface StateMachineResolver { 14 | 15 | /** 16 | * Evaluate available events from current states of state-machine 17 | * 18 | * @param stateMachine state machine 19 | * 20 | * @return Events collection 21 | */ 22 | List getAvailableEvents(StateMachine stateMachine); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/antkorwin/statemachine/statemachine/resolver/StateMachineResolverImpl.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.statemachine.resolver; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.springframework.statemachine.StateContext; 5 | import org.springframework.statemachine.StateMachine; 6 | import org.springframework.statemachine.support.DefaultStateContext; 7 | import org.springframework.statemachine.transition.Transition; 8 | import org.springframework.statemachine.trigger.Trigger; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.List; 12 | 13 | import static java.util.stream.Collectors.toList; 14 | 15 | /** 16 | * Created by Korovin Anatolii on 02.06.2018. 17 | * 18 | * @author Korovin Anatolii 19 | * @version 1.0 20 | */ 21 | @Component 22 | public class StateMachineResolverImpl implements StateMachineResolver { 23 | 24 | @Override 25 | public List getAvailableEvents(StateMachine stateMachine) { 26 | 27 | return stateMachine.getTransitions() 28 | .stream() 29 | .filter(t -> isTransitionSourceFromCurrentState(t, stateMachine)) 30 | .filter(t -> evaluateGuardCondition(stateMachine, t)) 31 | .map(Transition::getTrigger) 32 | .map(Trigger::getEvent) 33 | .collect(toList()); 34 | } 35 | 36 | 37 | private boolean isTransitionSourceFromCurrentState(Transition transition, 38 | StateMachine stateMachine) { 39 | 40 | return stateMachine.getState().getId() == transition.getSource().getId(); 41 | } 42 | 43 | 44 | private boolean evaluateGuardCondition(StateMachine stateMachine, 45 | Transition transition) { 46 | 47 | if (transition.getGuard() == null) { 48 | return true; 49 | } 50 | 51 | StateContext context = makeStateContext(stateMachine, transition); 52 | 53 | try { 54 | return transition.getGuard().evaluate(context); 55 | } catch (Exception e) { 56 | return false; 57 | } 58 | } 59 | 60 | 61 | @NotNull 62 | private DefaultStateContext makeStateContext(StateMachine stateMachine, 63 | Transition transition) { 64 | 65 | return new DefaultStateContext<>(StateContext.Stage.TRANSITION, 66 | null, 67 | null, 68 | stateMachine.getExtendedState(), 69 | transition, 70 | stateMachine, 71 | stateMachine.getState(), 72 | transition.getTarget(), 73 | null); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.output.ansi.enabled=always -------------------------------------------------------------------------------- /src/test/java/com/antkorwin/statemachine/BaseMongoIT.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.data.mongodb.core.MongoTemplate; 7 | import org.springframework.test.context.junit4.SpringRunner; 8 | import org.testcontainers.containers.GenericContainer; 9 | 10 | /** 11 | * Created by Korovin A. on 28.03.2018. 12 | *

13 | * Base Integration Test with 14 | * MongoDb started in TestContainers 15 | * 16 | * @author Korovin Anatoliy 17 | * @version 1.0 18 | */ 19 | @SpringBootTest 20 | @RunWith(SpringRunner.class) 21 | public abstract class BaseMongoIT { 22 | 23 | private static final Integer MONGO_PORT = 27017; 24 | private static GenericContainer mongo = new GenericContainer("mongo:latest") 25 | .withExposedPorts(MONGO_PORT); 26 | 27 | static { 28 | mongo.start(); 29 | System.setProperty("spring.data.mongodb.host", mongo.getContainerIpAddress()); 30 | System.setProperty("spring.data.mongodb.port", mongo.getMappedPort(MONGO_PORT).toString()); 31 | } 32 | 33 | @Autowired 34 | protected MongoTemplate mongoTemplate; 35 | } -------------------------------------------------------------------------------- /src/test/java/com/antkorwin/statemachine/InMemoryPersistTest.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine; 2 | 3 | import com.antkorwin.statemachine.statemachine.Events; 4 | import com.antkorwin.statemachine.statemachine.States; 5 | import org.assertj.core.api.Assertions; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.statemachine.StateMachine; 11 | import org.springframework.statemachine.config.StateMachineFactory; 12 | import org.springframework.statemachine.persist.StateMachinePersister; 13 | import org.springframework.test.context.ActiveProfiles; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import java.util.UUID; 17 | 18 | /** 19 | * Created by Korovin Anatolii on 06.05.2018. 20 | * 21 | * @author Korovin Anatolii 22 | * @version 1.0 23 | */ 24 | @RunWith(SpringRunner.class) 25 | @SpringBootTest 26 | @ActiveProfiles("in-memory") 27 | public class InMemoryPersistTest { 28 | 29 | @Autowired 30 | private StateMachinePersister persister; 31 | 32 | @Autowired 33 | private StateMachineFactory stateMachineFactory; 34 | 35 | @Test 36 | public void testInMemoryPersist() throws Exception { 37 | // Arrange 38 | StateMachine firstStateMachine = stateMachineFactory.getStateMachine(); 39 | firstStateMachine.sendEvent(Events.START_FEATURE); 40 | firstStateMachine.sendEvent(Events.DEPLOY); 41 | 42 | StateMachine secondStateMachine = stateMachineFactory.getStateMachine(); 43 | 44 | // Check Precondition 45 | Assertions.assertThat(firstStateMachine.getState().getId()).isEqualTo(States.IN_PROGRESS); 46 | Assertions.assertThat((boolean) firstStateMachine.getExtendedState().getVariables().get("deployed")) 47 | .isEqualTo(true); 48 | Assertions.assertThat(secondStateMachine.getState().getId()).isEqualTo(States.BACKLOG); 49 | Assertions.assertThat(secondStateMachine.getExtendedState().getVariables().get("deployed")).isNull(); 50 | 51 | // Act 52 | persister.persist(firstStateMachine, firstStateMachine.getUuid()); 53 | persister.restore(secondStateMachine, firstStateMachine.getUuid()); 54 | 55 | // Asserts 56 | Assertions.assertThat(secondStateMachine.getState().getId()).isEqualTo(States.IN_PROGRESS); 57 | Assertions.assertThat((boolean) secondStateMachine.getExtendedState().getVariables().get("deployed")) 58 | .isEqualTo(true); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/antkorwin/statemachine/MongoPersistTest.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine; 2 | 3 | import com.antkorwin.statemachine.statemachine.Events; 4 | import com.antkorwin.statemachine.statemachine.States; 5 | import org.assertj.core.api.Assertions; 6 | import org.bson.Document; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.statemachine.StateMachine; 11 | import org.springframework.statemachine.config.StateMachineFactory; 12 | import org.springframework.statemachine.persist.StateMachinePersister; 13 | import org.springframework.test.context.ActiveProfiles; 14 | 15 | import java.util.List; 16 | import java.util.Set; 17 | import java.util.UUID; 18 | 19 | /** 20 | * Created by Korovin Anatolii on 06.05.2018. 21 | * 22 | * @author Korovin Anatolii 23 | * @version 1.0 24 | */ 25 | @ActiveProfiles("mongo") 26 | public class MongoPersistTest extends BaseMongoIT { 27 | 28 | @Autowired 29 | private StateMachinePersister persister; 30 | @Autowired 31 | private StateMachineFactory stateMachineFactory; 32 | 33 | @Before 34 | public void setUp() throws Exception { 35 | mongoTemplate.getCollectionNames() 36 | .forEach(mongoTemplate::dropCollection); 37 | } 38 | 39 | @Test 40 | public void firstTest() { 41 | // Arrange 42 | // Act 43 | Set collectionNames = mongoTemplate.getCollectionNames(); 44 | // Asserts 45 | Assertions.assertThat(collectionNames).isEmpty(); 46 | } 47 | 48 | @Test 49 | public void testMongoPersist() throws Exception { 50 | // Arrange 51 | StateMachine firstStateMachine = stateMachineFactory.getStateMachine(); 52 | firstStateMachine.sendEvent(Events.START_FEATURE); 53 | firstStateMachine.sendEvent(Events.DEPLOY); 54 | 55 | StateMachine secondStateMachine = stateMachineFactory.getStateMachine(); 56 | 57 | // Check Precondition 58 | Assertions.assertThat(firstStateMachine.getState().getId()).isEqualTo(States.IN_PROGRESS); 59 | Assertions.assertThat((boolean) firstStateMachine.getExtendedState().getVariables().get("deployed")) 60 | .isEqualTo(true); 61 | Assertions.assertThat(secondStateMachine.getState().getId()).isEqualTo(States.BACKLOG); 62 | Assertions.assertThat(secondStateMachine.getExtendedState().getVariables().get("deployed")).isNull(); 63 | 64 | // Act 65 | persister.persist(firstStateMachine, firstStateMachine.getUuid()); 66 | persister.persist(secondStateMachine, secondStateMachine.getUuid()); 67 | persister.restore(secondStateMachine, firstStateMachine.getUuid()); 68 | 69 | // Asserts 70 | Assertions.assertThat(secondStateMachine.getState().getId()) 71 | .isEqualTo(States.IN_PROGRESS); 72 | 73 | Assertions.assertThat((boolean) secondStateMachine.getExtendedState().getVariables().get("deployed")) 74 | .isEqualTo(true); 75 | 76 | // Mongo specific asserts: 77 | Assertions.assertThat(mongoTemplate.getCollectionNames()) 78 | .isNotEmpty(); 79 | List documents = mongoTemplate.findAll(Document.class, 80 | "MongoDbRepositoryStateMachine"); 81 | 82 | Assertions.assertThat(documents).hasSize(2); 83 | Assertions.assertThat(documents) 84 | .flatExtracting(Document::values) 85 | .contains(firstStateMachine.getUuid().toString(), 86 | secondStateMachine.getUuid().toString()) 87 | .contains(firstStateMachine.getState().getId().toString(), 88 | secondStateMachine.getState().getId().toString()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/antkorwin/statemachine/MultipleFactoriesTest.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine; 2 | 3 | import com.antkorwin.statemachine.statemachine.Events; 4 | import com.antkorwin.statemachine.statemachine.States; 5 | import com.antkorwin.statemachine.statemachine.resolver.StateMachineResolver; 6 | import org.assertj.core.api.Assertions; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Qualifier; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.statemachine.StateMachine; 14 | import org.springframework.statemachine.config.StateMachineFactory; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | 17 | import java.util.List; 18 | 19 | import static com.antkorwin.statemachine.statemachine.Events.FINISH_FEATURE; 20 | import static com.antkorwin.statemachine.statemachine.Events.ROCK_STAR_DOUBLE_TASK; 21 | import static com.antkorwin.statemachine.statemachine.Events.START_FEATURE; 22 | 23 | /** 24 | * Created by Korovin Anatolii on 06.05.2018. 25 | * 26 | * @author Korovin Anatolii 27 | * @version 1.0 28 | */ 29 | @RunWith(SpringRunner.class) 30 | @SpringBootTest 31 | public class MultipleFactoriesTest { 32 | 33 | @Autowired 34 | @Qualifier("secondStateMachineFactory") 35 | private StateMachineFactory secondStateMachineFactory; 36 | 37 | private StateMachine stateMachine; 38 | 39 | @Autowired 40 | private StateMachineResolver stateMachineResolver; 41 | 42 | @Before 43 | public void setUp() throws Exception { 44 | stateMachine = secondStateMachineFactory.getStateMachine(); 45 | } 46 | 47 | /** 48 | * Try to make a transition with the guard throws an exception 49 | */ 50 | @Test 51 | public void failInGuardTest() { 52 | // Arrange 53 | // Act 54 | stateMachine.sendEvent(START_FEATURE); 55 | // Asserts 56 | Assertions.assertThat(stateMachine.getState().getId()) 57 | .isEqualTo(States.BACKLOG); 58 | } 59 | 60 | /** 61 | * Try to make a transition when the action throws an exception 62 | */ 63 | @Test 64 | public void failInActionTest() { 65 | // Arrange 66 | // Act 67 | stateMachine.sendEvent(ROCK_STAR_DOUBLE_TASK); 68 | // Asserts 69 | Assertions.assertThat(2+2).isEqualTo(4); 70 | Assertions.assertThat(stateMachine.getState().getId()) 71 | .isEqualTo(States.BACKLOG); 72 | } 73 | 74 | 75 | @Test 76 | public void internalFailTest() { 77 | // Check precondition 78 | Assertions.assertThat(stateMachine.hasStateMachineError()).isFalse(); 79 | // Act 80 | stateMachine.sendEvent(FINISH_FEATURE); 81 | // Asserts 82 | Assertions.assertThat(stateMachine.getState().getId()) 83 | .isEqualTo(States.DONE); 84 | 85 | Assertions.assertThat(stateMachine.hasStateMachineError()).isTrue(); 86 | } 87 | 88 | /** 89 | * try to evaluate available events from state with exception in guard 90 | */ 91 | @Test 92 | public void testResolver() { 93 | // Arrange 94 | // Act 95 | List availableEvents = stateMachineResolver.getAvailableEvents(stateMachine); 96 | 97 | // Asserts 98 | Assertions.assertThat(availableEvents) 99 | .containsOnly(ROCK_STAR_DOUBLE_TASK, FINISH_FEATURE); 100 | } 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/antkorwin/statemachine/StatemachineApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine; 2 | 3 | import com.antkorwin.statemachine.statemachine.Events; 4 | import com.antkorwin.statemachine.statemachine.States; 5 | import org.assertj.core.api.Assertions; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Qualifier; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.statemachine.StateMachine; 13 | import org.springframework.statemachine.config.StateMachineFactory; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | @RunWith(SpringRunner.class) 17 | @SpringBootTest 18 | public class StatemachineApplicationTests { 19 | 20 | 21 | private StateMachine stateMachine; 22 | 23 | @Autowired 24 | private StateMachineFactory stateMachineFactory; 25 | 26 | @Before 27 | public void setUp() throws Exception { 28 | stateMachine = stateMachineFactory.getStateMachine(); 29 | } 30 | 31 | @Test 32 | public void contextLoads() { 33 | Assertions.assertThat(stateMachine).isNotNull(); 34 | } 35 | 36 | @Test 37 | public void initialStateTest() { 38 | // Asserts 39 | Assertions.assertThat(stateMachine.getInitialState().getId()).isEqualTo(States.BACKLOG); 40 | } 41 | 42 | @Test 43 | public void firstStepTest() { 44 | // Act 45 | stateMachine.sendEvent(Events.START_FEATURE); 46 | // Asserts 47 | Assertions.assertThat(stateMachine.getState().getId()).isEqualTo(States.IN_PROGRESS); 48 | } 49 | 50 | @Test 51 | public void testGreenWay() { 52 | // Arrange 53 | // Act 54 | stateMachine.sendEvent(Events.START_FEATURE); 55 | stateMachine.sendEvent(Events.DEPLOY); 56 | stateMachine.sendEvent(Events.FINISH_FEATURE); 57 | stateMachine.sendEvent(Events.QA_CHECKED_UC); 58 | // Asserts 59 | Assertions.assertThat(stateMachine.getState().getId()).isEqualTo(States.DONE); 60 | } 61 | 62 | @Test 63 | public void testWrongWay() { 64 | // Arrange 65 | // Act 66 | stateMachine.sendEvent(Events.START_FEATURE); 67 | stateMachine.sendEvent(Events.QA_CHECKED_UC); 68 | // Asserts 69 | Assertions.assertThat(stateMachine.getState().getId()).isEqualTo(States.IN_PROGRESS); 70 | } 71 | 72 | @Test 73 | public void rockStarTest() { 74 | // Act 75 | stateMachine.sendEvent(Events.ROCK_STAR_DOUBLE_TASK); 76 | // Asserts 77 | Assertions.assertThat(stateMachine.getState().getId()).isEqualTo(States.TESTING); 78 | } 79 | 80 | @Test 81 | public void testingUnreachableWithoutDeploy() { 82 | // Arrange & Act 83 | stateMachine.sendEvent(Events.START_FEATURE); 84 | stateMachine.sendEvent(Events.FINISH_FEATURE); 85 | stateMachine.sendEvent(Events.QA_CHECKED_UC); // not accepted! 86 | // Asserts 87 | Assertions.assertThat(stateMachine.getState().getId()).isEqualTo(States.IN_PROGRESS); 88 | } 89 | 90 | @Test 91 | public void testDeployFromBacklog() { 92 | // Arrange 93 | // Act 94 | stateMachine.sendEvent(Events.DEPLOY); 95 | // Asserts 96 | Assertions.assertThat(stateMachine.getState().getId()).isEqualTo(States.BACKLOG); 97 | Assertions.assertThat(stateMachine.getExtendedState().getVariables().get("deployed")) 98 | .isEqualTo(true); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/antkorwin/statemachine/statemachine/resolver/StateMachineResolverIT.java: -------------------------------------------------------------------------------- 1 | package com.antkorwin.statemachine.statemachine.resolver; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import com.antkorwin.statemachine.statemachine.Events; 6 | import com.antkorwin.statemachine.statemachine.States; 7 | import org.assertj.core.api.Assertions; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.statemachine.StateMachine; 14 | import org.springframework.statemachine.config.StateMachineFactory; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * Created by Korovin Anatolii on 02.06.2018. 21 | * 22 | * @author Korovin Anatolii 23 | * @version 1.0 24 | */ 25 | @RunWith(SpringRunner.class) 26 | @SpringBootTest 27 | public class StateMachineResolverIT { 28 | 29 | private StateMachine stateMachine; 30 | 31 | @Autowired 32 | private StateMachineFactory stateMachineFactory; 33 | 34 | 35 | 36 | @Before 37 | public void setUp() throws Exception { 38 | stateMachine = stateMachineFactory.getStateMachine(); 39 | } 40 | 41 | @Autowired 42 | private StateMachineResolver stateMachineResolver; 43 | 44 | 45 | @Test 46 | public void testResolverWithoutGuard() { 47 | // Arrange 48 | // Act 49 | List availableEvents = stateMachineResolver.getAvailableEvents(stateMachine); 50 | // Asserts 51 | Assertions.assertThat(availableEvents) 52 | .containsOnly(Events.START_FEATURE, 53 | Events.ROCK_STAR_DOUBLE_TASK, 54 | Events.DEPLOY); 55 | } 56 | 57 | @Test 58 | public void testResolverWithGuard() { 59 | // Arrange 60 | stateMachine.sendEvent(Events.START_FEATURE); 61 | // Act 62 | List availableEvents = stateMachineResolver.getAvailableEvents(stateMachine); 63 | // Asserts 64 | Assertions.assertThat(availableEvents) 65 | .containsOnly(Events.DEPLOY); 66 | } 67 | } -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.output.ansi.enabled=always 2 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /uml/state_graph.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | [*] --> BACKLOG 4 | 5 | BACKLOG --> INPROGRESS : start feature 6 | 7 | INPROGRESS --> TESTING : finish feature 8 | 9 | TESTING -->DONE : QA checked UC 10 | 11 | TESTING --> INPROGRESS : QA reject 12 | 13 | DONE --> [*] 14 | 15 | BACKLOG --> TESTING : super star 16 | 17 | BACKLOG --> BACKLOG : deploy 18 | INPROGRESS --> INPROGRESS : deploy 19 | 20 | @enduml --------------------------------------------------------------------------------