From 28a23cbf15682dba9b38d7389a37f48fdfee491c Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 5 Mar 2016 11:28:34 +0000 Subject: [PATCH] Timer interval reset for states - Add new features around Trigger to arm/disarm it so that state entry/exit can stop firing TimerTrigger. Default functionality for timer(long) still stays same, however timerOnce(long) will kick this new functionality where trigger will fire once after state has been entered. - Fixes #165 --- .../config/AbstractStateMachineFactory.java | 5 +- .../StateMachineTransitionBuilder.java | 4 +- .../AbstractTransitionConfigurer.java | 9 ++ .../DefaultExternalTransitionConfigurer.java | 9 +- .../DefaultInternalTransitionConfigurer.java | 9 +- .../DefaultLocalTransitionConfigurer.java | 9 +- .../configurers/TransitionConfigurer.java | 8 + .../config/model/TransitionData.java | 16 +- .../statemachine/state/AbstractState.java | 23 ++- .../statemachine/state/CountTrigger.java | 138 ++++++++++++++++++ .../statemachine/state/ObjectState.java | 2 + .../support/DefaultStateMachineExecutor.java | 4 +- .../statemachine/trigger/EventTrigger.java | 9 ++ .../statemachine/trigger/TimerTrigger.java | 77 ++++++++-- .../statemachine/trigger/Trigger.java | 13 +- .../trigger/TimerTriggerTests.java | 90 ++++++++++++ .../java/demo/cdplayer/CdPlayerTests.java | 2 +- 17 files changed, 401 insertions(+), 26 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CountTrigger.java diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index 22206f18..84d41d26 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -51,6 +51,7 @@ import org.springframework.statemachine.ensemble.DistributedStateMachine; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.security.StateMachineSecurityInterceptor; +import org.springframework.statemachine.state.AbstractState; import org.springframework.statemachine.state.ChoicePseudoState; import org.springframework.statemachine.state.ChoicePseudoState.ChoiceStateData; import org.springframework.statemachine.state.DefaultPseudoState; @@ -547,12 +548,13 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS S target = transitionData.getTarget(); E event = transitionData.getEvent(); Long period = transitionData.getPeriod(); + Integer count = transitionData.getCount(); Trigger trigger = null; if (event != null) { trigger = new EventTrigger(event); } else if (period != null) { - TimerTrigger t = new TimerTrigger(period); + TimerTrigger t = new TimerTrigger(period, count != null ? count : 0); if (beanFactory != null) { t.setBeanFactory(beanFactory); } @@ -563,6 +565,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS t.setTaskScheduler(taskScheduler); } trigger = t; + ((AbstractState)stateMap.get(source)).getTriggers().add(trigger); } if (transitionData.getKind() == TransitionKind.EXTERNAL) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java index 0e97de28..678f26b7 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java @@ -111,7 +111,7 @@ public class StateMachineTransitionBuilder return apply(new DefaultJoinTransitionConfigurer()); } - public void add(S source, S target, S state, E event, Long period, Collection> actions, + public void add(S source, S target, S state, E event, Long period, Integer count, Collection> actions, Guard guard, TransitionKind kind, SecurityRule securityRule) { // if rule not given, get it from global if (securityRule == null) { @@ -119,7 +119,7 @@ public class StateMachineTransitionBuilder StateMachineConfigurationConfig config = getSharedObject(StateMachineConfigurationConfig.class); securityRule = config.getTransitionSecurityRule(); } - transitionData.add(new TransitionData(source, target, state, event, period, actions, guard, kind, securityRule)); + transitionData.add(new TransitionData(source, target, state, event, period, count, actions, guard, kind, securityRule)); } public void add(S source, List> choices) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java index c5475a8c..c5c84949 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java @@ -43,6 +43,7 @@ public abstract class AbstractTransitionConfigurer extends private S state; private E event; private Long period; + private Integer count; private final Collection> actions = new ArrayList>(); private Guard guard; private SecurityRule securityRule; @@ -67,6 +68,10 @@ public abstract class AbstractTransitionConfigurer extends return period; } + public Integer getCount() { + return count; + } + protected Collection> getActions() { return actions; } @@ -99,6 +104,10 @@ public abstract class AbstractTransitionConfigurer extends this.period = period; } + public void setCount(Integer count) { + this.count = count; + } + protected void addAction(Action action) { this.actions.add(action); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java index c4ffd218..edd9c103 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java @@ -38,7 +38,7 @@ public class DefaultExternalTransitionConfigurer extends AbstractTransitio @Override public void configure(StateMachineTransitionBuilder builder) throws Exception { - builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.EXTERNAL, + builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getCount(), getActions(), getGuard(), TransitionKind.EXTERNAL, getSecurityRule()); } @@ -72,6 +72,13 @@ public class DefaultExternalTransitionConfigurer extends AbstractTransitio return this; } + @Override + public ExternalTransitionConfigurer timerOnce(long period) { + setPeriod(period); + setCount(1); + return this; + } + @Override public ExternalTransitionConfigurer action(Action action) { addAction(action); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java index 6d707207..54e7ae8d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java @@ -38,7 +38,7 @@ public class DefaultInternalTransitionConfigurer extends AbstractTransitio @Override public void configure(StateMachineTransitionBuilder builder) throws Exception { - builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.INTERNAL, + builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getCount(), getActions(), getGuard(), TransitionKind.INTERNAL, getSecurityRule()); } @@ -66,6 +66,13 @@ public class DefaultInternalTransitionConfigurer extends AbstractTransitio return this; } + @Override + public InternalTransitionConfigurer timerOnce(long period) { + setPeriod(period); + setCount(1); + return this; + } + @Override public InternalTransitionConfigurer action(Action action) { addAction(action); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java index e9e433a9..40bb1deb 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java @@ -37,7 +37,7 @@ public class DefaultLocalTransitionConfigurer extends AbstractTransitionCo @Override public void configure(StateMachineTransitionBuilder builder) throws Exception { - builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.LOCAL, + builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getCount(), getActions(), getGuard(), TransitionKind.LOCAL, getSecurityRule()); } @@ -71,6 +71,13 @@ public class DefaultLocalTransitionConfigurer extends AbstractTransitionCo return this; } + @Override + public LocalTransitionConfigurer timerOnce(long period) { + setPeriod(period); + setCount(1); + return this; + } + @Override public LocalTransitionConfigurer action(Action action) { addAction(action); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java index ee984095..68c0e5c3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java @@ -67,6 +67,14 @@ public interface TransitionConfigurer extends */ T timer(long period); + /** + * Specify that this transition is triggered once by a time after a delay. + * + * @param period timer period in millis + * @return configurer for chaining + */ + T timerOnce(long period); + /** * Specify {@link Action} for this {@link Transition}. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionData.java index 9e0fb987..dcad8352 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionData.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 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. @@ -34,6 +34,7 @@ public class TransitionData { private final S state; private final E event; private final Long period; + private final Integer count; private final Collection> actions; private final Guard guard; private final TransitionKind kind; @@ -47,18 +48,20 @@ public class TransitionData { * @param state the state * @param event the event * @param period the period + * @param count the count * @param actions the actions * @param guard the guard * @param kind the kind * @param securityRule the security rule */ - public TransitionData(S source, S target, S state, E event, Long period, Collection> actions, + public TransitionData(S source, S target, S state, E event, Long period, Integer count, Collection> actions, Guard guard, TransitionKind kind, SecurityRule securityRule) { this.source = source; this.target = target; this.state = state; this.event = event; this.period = period; + this.count = count; this.actions = actions; this.guard = guard; this.kind = kind; @@ -110,6 +113,15 @@ public class TransitionData { return period; } + /** + * Gets the count. + * + * @return the count + */ + public Integer getCount() { + return count; + } + /** * Gets the actions. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java index d7a91cc2..3c662449 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java @@ -17,12 +17,14 @@ package org.springframework.statemachine.state; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import org.springframework.messaging.Message; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.trigger.Trigger; /** * Base implementation of a {@link State}. @@ -41,6 +43,7 @@ public abstract class AbstractState implements State { private final Collection> exitActions; private final Collection> regions = new ArrayList>(); private final StateMachine submachine; + private List> triggers = new ArrayList>(); /** * Instantiates a new abstract state. @@ -158,10 +161,18 @@ public abstract class AbstractState implements State { } @Override - public abstract void exit(StateContext context); + public void exit(StateContext context) { + for (Trigger trigger : triggers) { + trigger.disarm(); + } + } @Override - public abstract void entry(StateContext context); + public void entry(StateContext context) { + for (Trigger trigger : triggers) { + trigger.arm(); + } + } @Override public S getId() { @@ -232,6 +243,14 @@ public abstract class AbstractState implements State { return regions; } + public void setTriggers(List> triggers) { + this.triggers = triggers; + } + + public List> getTriggers() { + return triggers; + } + @Override public String toString() { return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CountTrigger.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CountTrigger.java new file mode 100644 index 00000000..dd3e8b97 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CountTrigger.java @@ -0,0 +1,138 @@ +/* + * Copyright 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.state; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.scheduling.support.PeriodicTrigger; +import org.springframework.util.Assert; + +/** + * Enhanced implementation following same logic from {@link PeriodicTrigger} + * except also adding a counter how many times a trigger can fire. If given + * count is either zero on a negative value, counter functionality is disabled. + * + * @author Janne Valkealahti + * @see PeriodicTrigger + */ +public class CountTrigger implements Trigger { + + private final int count; + private final long period; + private final TimeUnit timeUnit; + private volatile long initialDelay = 0; + private volatile boolean fixedRate = false; + private volatile int counter = 0; + + /** + * Create a trigger with the given period in milliseconds and firing + * exactly one time. + * + * @param period the period + */ + public CountTrigger(long period) { + this(1, period, null); + } + + /** + * Create a trigger with the given count, period and time unit. The time unit will + * apply not only to the period but also to any 'initialDelay' value, if + * configured on this Trigger later via {@link #setInitialDelay(long)}. + * + * @param count the count + * @param period the period + * @param timeUnit the time unit + */ + public CountTrigger(int count, long period, TimeUnit timeUnit) { + Assert.isTrue(period >= 0, "period must not be negative"); + Assert.isTrue(count >= 0, "count must not be negative"); + this.timeUnit = (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS); + this.period = this.timeUnit.toMillis(period); + this.count = count; + } + + /** + * Specify the delay for the initial execution. It will be evaluated in + * terms of this trigger's {@link TimeUnit}. If no time unit was explicitly + * provided upon instantiation, the default is milliseconds. + * + * @param initialDelay the new initial delay + */ + public void setInitialDelay(long initialDelay) { + this.initialDelay = this.timeUnit.toMillis(initialDelay); + } + + /** + * Specify whether the periodic interval should be measured between the + * scheduled start times rather than between actual completion times. + * The latter, "fixed delay" behavior, is the default. + * + * @param fixedRate the new fixed rate + */ + public void setFixedRate(boolean fixedRate) { + this.fixedRate = fixedRate; + } + + @Override + public Date nextExecutionTime(TriggerContext triggerContext) { + if (count > 0) { + if (++counter > count) { + return null; + } + } + if (triggerContext.lastScheduledExecutionTime() == null) { + return new Date(System.currentTimeMillis() + this.initialDelay); + } + else if (this.fixedRate) { + return new Date(triggerContext.lastScheduledExecutionTime().getTime() + this.period); + } + return new Date(triggerContext.lastCompletionTime().getTime() + this.period); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + count; + result = prime * result + (fixedRate ? 1231 : 1237); + result = prime * result + (int) (initialDelay ^ (initialDelay >>> 32)); + result = prime * result + (int) (period ^ (period >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CountTrigger other = (CountTrigger) obj; + if (count != other.count) + return false; + if (fixedRate != other.fixedRate) + return false; + if (initialDelay != other.initialDelay) + return false; + if (period != other.period) + return false; + return true; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java index ccf155ef..f0521220 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java @@ -123,6 +123,7 @@ public class ObjectState extends AbstractSimpleState { @Override public void exit(StateContext context) { + super.exit(context); Collection> actions = getExitActions(); if (actions != null) { for (Action action : actions) { @@ -137,6 +138,7 @@ public class ObjectState extends AbstractSimpleState { @Override public void entry(StateContext context) { + super.entry(context); Collection> actions = getEntryActions(); if (actions != null) { for (Action action : actions) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java index 05b5ff99..303984a3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -416,7 +416,9 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im ((TimerTrigger) trigger).addTriggerListener(new TriggerListener() { @Override public void triggered() { - log.debug("TimedTrigger triggered " + trigger); + if (log.isDebugEnabled()) { + log.debug("TimedTrigger triggered " + trigger); + } triggerQueue.add(new TriggerQueueItem(trigger, null)); scheduleEventQueueProcessing(); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/EventTrigger.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/EventTrigger.java index f6ef07f4..6e259cde 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/EventTrigger.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/EventTrigger.java @@ -40,4 +40,13 @@ public class EventTrigger implements Trigger { return event; } + @Override + public void arm() { + // no-opt + } + + @Override + public void disarm() { + // no-opt + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/TimerTrigger.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/TimerTrigger.java index 77bf96cb..9deefd99 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/TimerTrigger.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/TimerTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 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. @@ -16,19 +16,45 @@ package org.springframework.statemachine.trigger; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.springframework.statemachine.state.CountTrigger; import org.springframework.statemachine.support.LifecycleObjectSupport; +/** + * Implementation of a {@link Trigger} capable of firing on a + * static periods. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ public class TimerTrigger extends LifecycleObjectSupport implements Trigger { private final CompositeTriggerListener triggerListener = new CompositeTriggerListener(); - private final long period; - + private final int count; private volatile ScheduledFuture scheduled; + /** + * Instantiates a new timer trigger. + * + * @param period the period in milliseconds + */ public TimerTrigger(long period) { + this(period, 0); + } + + /** + * Instantiates a new timer trigger. + * + * @param period the period + * @param count the count + */ + public TimerTrigger(long period, int count) { this.period = period; + this.count = count; } @Override @@ -48,25 +74,50 @@ public class TimerTrigger extends LifecycleObjectSupport implements Trigge @Override protected void doStart() { - scheduled = getTaskScheduler().scheduleAtFixedRate(new Runnable() { + if (count > 0) { + return; + } + schedule(); + } + + @Override + protected void doStop() { + cancel(); + } + + @Override + public void arm() { + if (scheduled != null) { + return; + } + schedule(); + } + + @Override + public void disarm() { + if (count > 0) { + cancel(); + } + } + + private void schedule() { + scheduled = getTaskScheduler().schedule(new Runnable() { @Override public void run() { notifyTriggered(); } - }, period); - } - - @Override - protected void doStop() { - if (scheduled != null) { - scheduled.cancel(true); - } - scheduled = null; + }, new CountTrigger(count, period, TimeUnit.MILLISECONDS)); } private void notifyTriggered() { triggerListener.triggered(); } + private void cancel() { + if (scheduled != null) { + scheduled.cancel(true); + } + scheduled = null; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/Trigger.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/Trigger.java index 253f1004..614ba629 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/Trigger.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/trigger/Trigger.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 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. @@ -51,4 +51,15 @@ public interface Trigger { */ E getEvent(); + /** + * Arm a trigger. After trigger has been armed a {@link TriggerListener} + * may receive events. + */ + void arm(); + + /** + * Disarm a trigger. After trigger has been disarmed a {@link TriggerListener} + * will not receive events. + */ + void disarm(); } 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 4634dee8..2d4865cb 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 @@ -18,8 +18,11 @@ 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.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; +import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -30,6 +33,7 @@ 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.TestUtils; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; @@ -119,6 +123,55 @@ public class TimerTriggerTests extends AbstractStateMachineTests { } } + private class TestTriggerListener implements TriggerListener { + + CountDownLatch latch = new CountDownLatch(1); + + @Override + public void triggered() { + latch.countDown(); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testTimerDelayFireOnlyOnState() throws Exception { + context.register(BaseConfig.class, Config4.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachine.class); + TestTimerAction action = context.getBean("testTimerAction", TestTimerAction.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + + TimerTrigger trigger = null; + Map, Transition> triggerToTransitionMap = TestUtils.readField("triggerToTransitionMap", machine); + for (Entry, Transition> entry : triggerToTransitionMap.entrySet()) { + if (entry.getKey() instanceof TimerTrigger) { + trigger = (TimerTrigger) entry.getKey(); + continue; + } + } + assertThat(trigger, notNullValue()); + TestTriggerListener tlistener = new TestTriggerListener(); + trigger.addTriggerListener(tlistener); + + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1)); + + assertThat(tlistener.latch.await(2, TimeUnit.SECONDS), is(false)); + + 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)); + + assertThat(action.latch.await(2, TimeUnit.SECONDS), is(true)); + action.reset(1); + assertThat(action.latch.await(2, TimeUnit.SECONDS), is(false)); + } + static class Config1 { @Bean @@ -211,6 +264,39 @@ public class TimerTriggerTests extends AbstractStateMachineTests { } + @Configuration + @EnableStateMachine + static class Config4 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()) + .timerOnce(1000); + } + + @Bean + public TestTimerAction testTimerAction() { + return new TestTimerAction(); + } + + } + private static class TestTimerAction implements Action { int count = 0; @@ -222,6 +308,10 @@ public class TimerTriggerTests extends AbstractStateMachineTests { latch.countDown(); } + void reset(int a) { + latch = new CountDownLatch(a); + count = 0; + } } private static class TestTimerAction2 implements Action { diff --git a/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java b/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java index 1c8801dd..38126f61 100644 --- a/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java +++ b/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java @@ -182,7 +182,7 @@ public class CdPlayerTests { assertThat(listener.transitionCount, is(1)); listener.reset(0, 0, 0, 2); - assertThat(listener.transitionLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.transitionLatch.await(2100, TimeUnit.MILLISECONDS), is(true)); assertThat(listener.transitionCount, is(2)); assertLcdStatusNotContains("00:02"); }