/*
* 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();
}
}
}