Add TimerTrigger

- This fixes #gh21
- New TimerTrigger which can be used to schedule
  triggers which then may cause transitions.
This commit is contained in:
Janne Valkealahti
2015-03-14 11:01:59 +00:00
parent 1e3fb04614
commit b9b270bf46
16 changed files with 417 additions and 15 deletions

View File

@@ -45,6 +45,8 @@ import org.springframework.statemachine.transition.DefaultInternalTransition;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.transition.TransitionKind;
import org.springframework.statemachine.trigger.EventTrigger;
import org.springframework.statemachine.trigger.TimerTrigger;
import org.springframework.statemachine.trigger.Trigger;
import org.springframework.util.ObjectUtils;
/**
@@ -289,14 +291,27 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
S source = transitionData.getSource();
S target = transitionData.getTarget();
E event = transitionData.getEvent();
Long period = transitionData.getPeriod();
Trigger<S, E> trigger = null;
if (event != null) {
trigger = new EventTrigger<S, E>(event);
} else if (period != null) {
TimerTrigger<S, E> t = new TimerTrigger<S, E>(period);
if (beanFactory != null) {
t.setBeanFactory(beanFactory);
}
trigger = t;
}
if (transitionData.getKind() == TransitionKind.EXTERNAL) {
DefaultExternalTransition<S, E> transition = new DefaultExternalTransition<S, E>(stateMap.get(source),
stateMap.get(target), transitionData.getActions(), event, transitionData.getGuard(), event != null ? new EventTrigger<S, E>(event) : null);
stateMap.get(target), transitionData.getActions(), event, transitionData.getGuard(), trigger);
transitions.add(transition);
} else if (transitionData.getKind() == TransitionKind.INTERNAL) {
DefaultInternalTransition<S, E> transition = new DefaultInternalTransition<S, E>(stateMap.get(source),
transitionData.getActions(), event, transitionData.getGuard(), event != null ? new EventTrigger<S, E>(event) : null);
transitionData.getActions(), event, transitionData.getGuard(), trigger);
transitions.add(transition);
}
}

View File

@@ -72,8 +72,8 @@ public class StateMachineTransitionBuilder<S, E>
return apply(new DefaultLocalTransitionConfigurer<S, E>());
}
public void add(S source, S target, S state, E event, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
transitionData.add(new TransitionData<S, E>(source, target, state, event, actions, guard, kind));
public void add(S source, S target, S state, E event, Long period, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
transitionData.add(new TransitionData<S, E>(source, target, state, event, period, actions, guard, kind));
}
}

View File

@@ -38,14 +38,16 @@ public class StateMachineTransitions<S, E> {
S target;
S state;
E event;
Long period;
Collection<Action<S, E>> actions;
Guard<S, E> guard;
TransitionKind kind;
public TransitionData(S source, S target, S state, E event, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
public TransitionData(S source, S target, S state, E event, Long period, Collection<Action<S, E>> actions, Guard<S, E> guard, TransitionKind kind) {
this.source = source;
this.target = target;
this.state = state;
this.event = event;
this.period = period;
this.actions = actions;
this.guard = guard;
this.kind = kind;
@@ -62,6 +64,9 @@ public class StateMachineTransitions<S, E> {
public E getEvent() {
return event;
}
public Long getPeriod() {
return period;
}
public Collection<Action<S, E>> getActions() {
return actions;
}

View File

@@ -50,13 +50,15 @@ public class DefaultExternalTransitionConfigurer<S, E>
private E event;
private Long period;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, state, event, actions, guard, TransitionKind.EXTERNAL);
builder.add(source, target, state, event, period, actions, guard, TransitionKind.EXTERNAL);
}
@Override
@@ -83,6 +85,12 @@ public class DefaultExternalTransitionConfigurer<S, E>
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> timer(long period) {
this.period = period;
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> action(Action<S, E> action) {
actions.add(action);

View File

@@ -50,13 +50,15 @@ public class DefaultInternalTransitionConfigurer<S, E>
private E event;
private Long period;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, state, event, actions, guard, TransitionKind.INTERNAL);
builder.add(source, target, state, event, period, actions, guard, TransitionKind.INTERNAL);
}
@Override
@@ -77,6 +79,12 @@ public class DefaultInternalTransitionConfigurer<S, E>
return this;
}
@Override
public InternalTransitionConfigurer<S, E> timer(long period) {
this.period = period;
return this;
}
@Override
public InternalTransitionConfigurer<S, E> action(Action<S, E> action) {
actions.add(action);

View File

@@ -50,13 +50,15 @@ public class DefaultLocalTransitionConfigurer<S, E>
private E event;
private Long period;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, state, event, actions, guard, TransitionKind.LOCAL);
builder.add(source, target, state, event, period, actions, guard, TransitionKind.LOCAL);
}
@Override
@@ -83,6 +85,12 @@ public class DefaultLocalTransitionConfigurer<S, E>
return this;
}
@Override
public LocalTransitionConfigurer<S, E> timer(long period) {
this.period = period;
return this;
}
@Override
public LocalTransitionConfigurer<S, E> action(Action<S, E> action) {
actions.add(action);

View File

@@ -51,6 +51,8 @@ public interface TransitionConfigurer<T, S, E> extends
*/
T event(E event);
T timer(long period);
/**
* Specify {@link Action} for this {@link Transition}.
*

View File

@@ -31,6 +31,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.Lifecycle;
import org.springframework.core.OrderComparator;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
@@ -50,7 +51,9 @@ import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.transition.TransitionKind;
import org.springframework.statemachine.trigger.DefaultTriggerContext;
import org.springframework.statemachine.trigger.TimerTrigger;
import org.springframework.statemachine.trigger.Trigger;
import org.springframework.statemachine.trigger.TriggerListener;
import org.springframework.util.Assert;
/**
@@ -204,6 +207,7 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
@Override
protected void doStart() {
super.doStart();
registerTriggerListener();
switchToState(initialState, initialEvent, null);
}
@@ -378,11 +382,17 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
while ((queueItem = triggerQueue.poll()) != null) {
Message<E> queuedEvent = queueItem.message;
Transition<S, E> transition = triggerToTransitionMap.get(queueItem.trigger);
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(queuedEvent.getHeaders(), extendedState, transition);
notifyTransitionStart(transition);
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(queuedEvent != null ? queuedEvent.getHeaders() : null, extendedState, transition);
if (transition == null) {
continue;
}
State<S,E> source = transition.getSource();
if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
continue;
}
notifyTransitionStart(transition);
boolean transit = transition.transit(stateContext);
if (transit && transition.getKind() != TransitionKind.INTERNAL) {
switchToState(transition.getTarget(), queuedEvent, transition);
@@ -459,6 +469,24 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
return handlersList;
}
private void registerTriggerListener() {
for (final Trigger<S, E> trigger : triggerToTransitionMap.keySet()) {
if (trigger instanceof TimerTrigger) {
((TimerTrigger<?, ?>)trigger).addTriggerListener(new TriggerListener() {
@Override
public void triggered() {
log.debug("TimedTrigger triggered " + trigger);
triggerQueue.add(new TriggerQueueItem(trigger, null));
scheduleEventQueueProcessing();
}
});
}
if (trigger instanceof Lifecycle) {
((Lifecycle)trigger).start();
}
}
}
private void notifyStateChanged(State<S,E> source, State<S,E> target) {
stateListener.stateChanged(source, target);
StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();

View File

@@ -0,0 +1,38 @@
/*
* 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.trigger;
import java.util.Iterator;
import org.springframework.statemachine.listener.AbstractCompositeListener;
/**
* Default {@link TriggerListener} dispatcher.
*
* @author Janne Valkealahti
*
*/
public class CompositeTriggerListener extends AbstractCompositeListener<TriggerListener> implements TriggerListener {
@Override
public void triggered() {
for (Iterator<TriggerListener> iterator = getListeners().reverse(); iterator.hasNext();) {
TriggerListener listener = iterator.next();
listener.triggered();
}
}
}

View File

@@ -30,4 +30,9 @@ public class EventTrigger<S, E> implements Trigger<S, E> {
return ObjectUtils.nullSafeEquals(event, context.getEvent());
}
@Override
public void addTriggerListener(TriggerListener listener) {
// no-opt
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.trigger;
import java.util.concurrent.ScheduledFuture;
import org.springframework.statemachine.support.LifecycleObjectSupport;
public class TimerTrigger<S, E> extends LifecycleObjectSupport implements Trigger<S, E> {
private final CompositeTriggerListener triggerListener = new CompositeTriggerListener();
private final long period;
private volatile ScheduledFuture<?> scheduled;
public TimerTrigger(long period) {
this.period = period;
}
@Override
public boolean evaluate(TriggerContext<S, E> context) {
return false;
}
@Override
public void addTriggerListener(TriggerListener listener) {
triggerListener.register(listener);
}
@Override
protected void doStart() {
scheduled = getTaskScheduler().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
notifyTriggered();
}
}, period);
}
@Override
protected void doStop() {
if (scheduled != null) {
scheduled.cancel(true);
}
scheduled = null;
}
private void notifyTriggered() {
triggerListener.triggered();
}
}

View File

@@ -36,4 +36,11 @@ public interface Trigger<S,E> {
*/
boolean evaluate(TriggerContext<S, E> context);
/**
* Adds the trigger listener.
*
* @param listener the listener
*/
void addTriggerListener(TriggerListener listener);
}

View File

@@ -0,0 +1,31 @@
/*
* 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.trigger;
/**
* {@code TriggerListener} for listening a {@link Trigger} events.
*
* @author Janne Valkealahti
*
*/
public interface TriggerListener {
/**
* Notified when trigger has been triggered.
*/
void triggered();
}

View File

@@ -28,6 +28,8 @@ 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.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
@@ -79,6 +81,11 @@ public abstract class AbstractStateMachineTests {
return new SyncTaskExecutor();
}
@Bean
public TaskScheduler taskScheduler() {
return new ConcurrentTaskScheduler();
}
}
public static class TestEntryAction extends AbstractTestAction {

View File

@@ -15,10 +15,13 @@
*/
package org.springframework.statemachine;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;
@@ -36,19 +39,65 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionCo
public class StateMachineTests extends AbstractStateMachineTests {
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
@Test
public void testLoggingEvents() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
context.register(Config1.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
EnumStateMachine<TestStates,TestEvents> machine =
ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.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());
ctx.close();
}
@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);
machine.start();
Thread.sleep(2000);
assertThat(testAction2.stateContexts.size(), is(0));
machine.sendEvent(TestEvents.E1);
assertThat(testAction1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(testAction1.stateContexts.size(), is(1));
assertThat(testAction2.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(testAction2.stateContexts.size(), is(1));
machine.sendEvent(TestEvents.E2);
assertThat(testAction3.onExecuteLatch.await(1, 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));
machine.sendEvent(TestEvents.E3);
assertThat(testAction4.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(testAction4.stateContexts.size(), is(1));
assertThat(testAction2.stateContexts.size(), is(timedTriggered));
}
private static class LoggingAction implements Action<TestStates, TestEvents> {
@@ -70,7 +119,7 @@ public class StateMachineTests extends AbstractStateMachineTests {
@Configuration
@EnableStateMachine
static class Config extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
@@ -124,4 +173,68 @@ public class StateMachineTests extends AbstractStateMachineTests {
}
@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();
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.trigger;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.statemachine.AbstractStateMachineTests;
public class TimerTriggerTests extends AbstractStateMachineTests {
@Test
public void testListenerEvents() throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class, Config1.class);
final CountDownLatch latch = new CountDownLatch(2);
@SuppressWarnings("rawtypes")
TimerTrigger timerTrigger = ctx.getBean(TimerTrigger.class);
timerTrigger.addTriggerListener(new TriggerListener() {
@Override
public void triggered() {
latch.countDown();
}
});
timerTrigger.afterPropertiesSet();
timerTrigger.start();
assertThat(latch.await(1, TimeUnit.SECONDS), is(true));
ctx.close();
}
static class Config1 {
@Bean
public TimerTrigger<TestStates, TestEvents> timerTrigger() {
return new TimerTrigger<TestStates, TestEvents>(100);
}
}
}