Java tutorial
/* * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.statemachine; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; public class StateMachineTests extends AbstractStateMachineTests { @Override protected AnnotationConfigApplicationContext buildContext() { return new AnnotationConfigApplicationContext(); } @Test public void testLoggingEvents() { context.register(Config1.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates, TestEvents> machine = context .getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); assertThat(machine, notNullValue()); machine.start(); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("foo", "jee1").build()); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E2).setHeader("foo", "jee2").build()); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E4).setHeader("foo", "jee2").build()); } @Test public void testTimerTransition() throws Exception { context.register(BaseConfig.class, Config2.class); context.refresh(); TestAction testAction1 = context.getBean("testAction1", TestAction.class); TestAction testAction2 = context.getBean("testAction2", TestAction.class); TestAction testAction3 = context.getBean("testAction3", TestAction.class); TestAction testAction4 = context.getBean("testAction4", TestAction.class); @SuppressWarnings("unchecked") StateMachine<TestStates, TestEvents> machine = context .getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); listener.reset(1); machine.start(); assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(1)); assertThat(testAction2.stateContexts.size(), is(0)); listener.reset(0, 1); machine.sendEvent(TestEvents.E1); assertThat(listener.transitionLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(testAction1.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(testAction1.stateContexts.size(), is(1)); assertThat(testAction2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(testAction2.stateContexts.size(), is(1)); listener.reset(0, 1); machine.sendEvent(TestEvents.E2); assertThat(listener.transitionLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(testAction3.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(testAction3.stateContexts.size(), is(1)); // timer still fires but should not cause transition anymore // after we sleep and do next event int timedTriggered = testAction2.stateContexts.size(); Thread.sleep(2000); assertThat(testAction2.stateContexts.size(), is(timedTriggered)); listener.reset(0, 1); machine.sendEvent(TestEvents.E3); assertThat(listener.transitionLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(testAction4.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(testAction4.stateContexts.size(), is(1)); assertThat(testAction2.stateContexts.size(), is(timedTriggered)); } @Test @SuppressWarnings("unchecked") public void testForkJoin() throws Exception { context.register(BaseConfig.class, Config3.class); context.refresh(); ObjectStateMachine<TestStates, TestEvents> machine = context .getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); assertThat(machine, notNullValue()); listener.reset(1); machine.start(); assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(1)); assertThat(machine.getState().getIds(), contains(TestStates.SI)); listener.reset(3); machine.sendEvent(TestEvents.E1); assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(3)); assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S20, TestStates.S30)); listener.reset(1); machine.sendEvent(TestEvents.E2); assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(1)); assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S21, TestStates.S30)); listener.reset(2); machine.sendEvent(TestEvents.E3); assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(2)); assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S4)); } @Test public void testStringStatesAndEvents() throws Exception { context.register(Config4.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") StateMachine<String, String> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); TestListener2 listener = new TestListener2(); machine.addStateListener(listener); assertThat(machine, notNullValue()); machine.start(); listener.reset(1); machine.sendEvent(MessageBuilder.withPayload("E1").setHeader("foo", "jee1").build()); assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(1)); assertThat(machine.getState().getIds(), containsInAnyOrder("S1")); } @Test public void testBackToItself() { context.register(BaseConfig.class, Config5.class); context.refresh(); @SuppressWarnings("unchecked") StateMachine<TestStates, TestEvents> machine = context .getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); assertThat(machine, notNullValue()); TestStateEntryExitListener listener = new TestStateEntryExitListener(); machine.addStateListener(listener); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.SI)); listener.reset(); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); assertThat(machine.getState().getIds(), contains(TestStates.SI)); assertThat(listener.exited.size(), is(1)); assertThat(listener.entered.size(), is(1)); } private static class LoggingAction implements Action<TestStates, TestEvents> { private static final Log log = LogFactory.getLog(StateMachineTests.LoggingAction.class); private String message; public LoggingAction(String message) { this.message = message; } @Override public void execute(StateContext<TestStates, TestEvents> context) { log.info("Hello from LoggingAction " + message + " foo=" + context.getMessageHeaders().get("foo")); } } @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states.withStates().initial(TestStates.S1).state(TestStates.S1).state(TestStates.S2) .state(TestStates.S3, TestEvents.E4).state(TestStates.S4); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions.withExternal().source(TestStates.S1).target(TestStates.S2).event(TestEvents.E1) .action(loggingAction()).action(loggingAction()).and().withExternal().source(TestStates.S2) .target(TestStates.S3).event(TestEvents.E2).action(loggingAction()).and().withExternal() .source(TestStates.S3).target(TestStates.S4).event(TestEvents.E3).action(loggingAction()).and() .withExternal().source(TestStates.S4).target(TestStates.S3).event(TestEvents.E4) .action(loggingAction()); } @Bean public LoggingAction loggingAction() { return new LoggingAction("as bean"); } @Bean public TaskExecutor taskExecutor() { return new SyncTaskExecutor(); } } @Configuration @EnableStateMachine static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states.withStates().initial(TestStates.S1).state(TestStates.S1).state(TestStates.S2) .state(TestStates.S3).state(TestStates.S4); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions.withExternal().source(TestStates.S1).target(TestStates.S2).event(TestEvents.E1) .action(testAction1()).and().withInternal().source(TestStates.S2).timer(1000) .action(testAction2()).and().withExternal().source(TestStates.S2).target(TestStates.S3) .event(TestEvents.E2).action(testAction3()).and().withExternal().source(TestStates.S3) .target(TestStates.S4).event(TestEvents.E3).action(testAction4()); } @Bean public TestAction testAction1() { return new TestAction(); } @Bean public TestAction testAction2() { return new TestAction(); } @Bean public TestAction testAction3() { return new TestAction(); } @Bean public TestAction testAction4() { return new TestAction(); } } @Configuration @EnableStateMachine static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states.withStates().initial(TestStates.SI).state(TestStates.SI).fork(TestStates.S1).state(TestStates.S2) .end(TestStates.SF).join(TestStates.S3).state(TestStates.S4).and().withStates() .parent(TestStates.S2).initial(TestStates.S20).state(TestStates.S20).state(TestStates.S21).and() .withStates().parent(TestStates.S2).initial(TestStates.S30).state(TestStates.S30) .state(TestStates.S31); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions.withExternal().source(TestStates.SI).target(TestStates.S2).event(TestEvents.E1).and() .withExternal().source(TestStates.S20).target(TestStates.S21).event(TestEvents.E2).and() .withExternal().source(TestStates.S30).target(TestStates.S31).event(TestEvents.E3).and() .withFork().source(TestStates.S1).target(TestStates.S20).target(TestStates.S30).and().withJoin() .source(TestStates.S21).source(TestStates.S31).target(TestStates.S3).and().withExternal() .source(TestStates.S3).target(TestStates.S4); } } @Configuration @EnableStateMachine static class Config4 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states.withStates().initial("SI").state("S1").state("S2"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions.withExternal().source("SI").target("S1").event("E1"); } } @Configuration @EnableStateMachine static class Config5 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states.withStates().initial(TestStates.SI).states(EnumSet.allOf(TestStates.class)); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions.withExternal().source(TestStates.SI).target(TestStates.SI).event(TestEvents.E1); } } private static class TestListener extends StateMachineListenerAdapter<TestStates, TestEvents> { volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); volatile CountDownLatch transitionLatch = new CountDownLatch(0); volatile int stateChangedCount = 0; @Override public void stateChanged(State<TestStates, TestEvents> from, State<TestStates, TestEvents> to) { stateChangedCount++; stateChangedLatch.countDown(); } @Override public void transition(Transition<TestStates, TestEvents> transition) { transitionLatch.countDown(); } public void reset(int c1) { reset(c1, 0); } public void reset(int c1, int c2) { stateChangedLatch = new CountDownLatch(c1); transitionLatch = new CountDownLatch(c2); stateChangedCount = 0; } } private static class TestListener2 extends StateMachineListenerAdapter<String, String> { volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); volatile CountDownLatch transitionLatch = new CountDownLatch(0); volatile int stateChangedCount = 0; @Override public void stateChanged(State<String, String> from, State<String, String> to) { stateChangedCount++; stateChangedLatch.countDown(); } @Override public void transition(Transition<String, String> transition) { transitionLatch.countDown(); } public void reset(int c1) { reset(c1, 0); } public void reset(int c1, int c2) { stateChangedLatch = new CountDownLatch(c1); transitionLatch = new CountDownLatch(c2); stateChangedCount = 0; } } private static class TestStateEntryExitListener extends StateMachineListenerAdapter<TestStates, TestEvents> { List<State<TestStates, TestEvents>> entered = new ArrayList<>(); List<State<TestStates, TestEvents>> exited = new ArrayList<>(); @Override public void stateEntered(State<TestStates, TestEvents> state) { entered.add(state); } @Override public void stateExited(State<TestStates, TestEvents> state) { exited.add(state); } public void reset() { entered.clear(); exited.clear(); } } }