org.springframework.statemachine.StateMachineTests.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.statemachine.StateMachineTests.java

Source

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