diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index 2aeb31f6..10e96108 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -522,8 +522,23 @@ for example user is implementing actions. === TimerTrigger _TimerTrigger_ is useful when something needs to be triggered -automatically without any user interaction. Trigger is added to a -transition by associating a timer to it during a configuration. +automatically without any user interaction. `Trigger` is added to a +transition by associating a timer with it during a configuration. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests2.java[tags=snippetA] +---- + +In above we have two states, `S1` and `S2`. We have a normal external +transition from `S1` to `S2` with event `E1` but interesting part is +when we define internal transition with source state `S2` and +associate it with `Action` bean `timerAction` and `timer` value of +`1000ms`. Once a state machine receive event `E1` it does a transition +from `S1` to `S2` and timer kicks in. As long as state is kept in `S2` +`TimerTrigger` executes and causes a transition associated with that +state which in this case is the internal transition which has the +`timerAction` defined. [[sm-listeners]] == Listening State Machine Events diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java new file mode 100644 index 00000000..9440d641 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 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.docs; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +public class DocsConfigurationSampleTests2 extends AbstractStateMachineTests { + +// tag::snippetA[] + @Configuration + @EnableStateMachine + static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withInternal() + .source("S2") + .action(timerAction()) + .timer(1000); + } + + @Bean + public TimerAction timerAction() { + return new TimerAction(); + } + } + + static class TimerAction implements Action { + + @Override + public void execute(StateContext context) { + // do something in every 1 sec + } + } +// end::snippetA[] + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/trigger/TimerTriggerTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/trigger/TimerTriggerTests.java index 9004958f..e5bbfd46 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/trigger/TimerTriggerTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/trigger/TimerTriggerTests.java @@ -16,6 +16,8 @@ package org.springframework.statemachine.trigger; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertThat; import java.util.concurrent.CountDownLatch; @@ -24,17 +26,34 @@ import java.util.concurrent.TimeUnit; 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.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +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 TimerTriggerTests extends AbstractStateMachineTests { + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + @Test public void testListenerEvents() throws Exception { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class, Config1.class); + context.register(BaseConfig.class, Config1.class); + context.refresh(); final CountDownLatch latch = new CountDownLatch(2); @SuppressWarnings("rawtypes") - TimerTrigger timerTrigger = ctx.getBean(TimerTrigger.class); + TimerTrigger timerTrigger = context.getBean(TimerTrigger.class); timerTrigger.addTriggerListener(new TriggerListener() { @Override @@ -46,7 +65,31 @@ public class TimerTriggerTests extends AbstractStateMachineTests { timerTrigger.start(); assertThat(latch.await(1, TimeUnit.SECONDS), is(true)); - ctx.close(); + } + + @SuppressWarnings("unchecked") + @Test + public void testTimerTransitions() throws Exception { + context.register(BaseConfig.class, Config2.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachine.class); + TestTimerAction action = context.getBean("testTimerAction", TestTimerAction.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1)); + + listener.reset(1); + machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2)); + + Thread.sleep(1000); + // we should have 100, just test 90 due to timing + assertThat(action.count, greaterThan(90)); } static class Config1 { @@ -57,4 +100,83 @@ public class TimerTriggerTests extends AbstractStateMachineTests { } } + @Configuration + @EnableStateMachine + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withInternal() + .source(TestStates.S2) + .action(testTimerAction()) + .timer(10); + } + + @Bean + public TestTimerAction testTimerAction() { + return new TestTimerAction(); + } + + } + + private static class TestTimerAction implements Action { + + int count = 0; + + @Override + public void execute(StateContext context) { + count++; + } + + } + + private static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1); + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile CountDownLatch transitionLatch = new CountDownLatch(0); + volatile int stateChangedCount = 0; + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + stateMachineStartedLatch.countDown(); + } + + @Override + public void stateChanged(State from, State to) { + stateChangedCount++; + stateChangedLatch.countDown(); + } + + @Override + public void transition(Transition 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; + } + + } + }